blob: 069240c982ec6d77d293a7db598182021ecb08c8 [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::event::Fact;
use maka_agent_facts::event::InvocationInput;
use maka_agent_facts::event::InvocationOutcome;
use maka_agent_facts::event::LogScope;
use maka_agent_facts::model::ModelEvent;
use maka_agent_facts::model::ModelFinishReason;
use maka_agent_facts::model::ModelUsage;
use maka_agent_facts::session::CopyPurpose;
use maka_event_log::{
EventLog, StoreError,
sessions::{SessionCopy, SessionRetirement},
};
use serde_json::{Value, json};
#[path = "usage/fixture.rs"]
mod fixture;
use fixture::{event, query, request};
#[tokio::test]
async fn collection_waits_for_the_last_history_owner_and_preserves_frozen_accounting() {
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("material.sqlite");
let log = EventLog::open(&path).await.unwrap();
log.create_session("source", "create", &json!({}), 1)
.await
.unwrap();
let opening = event(
Fact::InvocationOpened {
configuration: None,
input: InvocationInput::Message {
content: "private-question".repeat(10000).into(),
source_messages: vec![],
request_fingerprint: None,
},
},
0,
);
log.append(&opening).await.unwrap();
log.append(&request("observed", 1000)).await.unwrap();
let mut frozen = query();
frozen.through = Some(log.model_attempts(query(), 0, 100).await.unwrap().through);
for i in 0..12 {
log.append(&event(
Fact::ModelObserved {
step_id: "observed".into(),
event: ModelEvent::PartDelta {
id: "part".into(),
text: "private-response".repeat(1000),
provider_options: None,
},
},
1100 + i,
))
.await
.unwrap();
}
log.append(&event(
Fact::ModelObserved {
step_id: "observed".into(),
event: ModelEvent::Finished {
reason: ModelFinishReason::Stop,
usage: ModelUsage {
input_tokens: Some(100),
output_tokens: Some(12),
..Default::default()
},
provider_options: None,
},
},
1200,
))
.await
.unwrap();
log.append(&event(
Fact::ModelInterrupted {
model_failure: Some(
maka_agent_facts::model::error::ModelFailureCause::Provider {
reason: maka_agent_facts::model::error::ProviderFailureReason::Network,
},
),
diagnostic: None,
step_id: "observed".into(),
status: maka_agent_facts::event::ModelInterruption::Failed,
},
1250,
))
.await
.unwrap();
// Provider usage is factual even when no response was accepted.
log.append(&event(
Fact::InvocationEnded {
outcome: InvocationOutcome::Failed {
model_failure: Some(
maka_agent_facts::model::error::ModelFailureCause::Provider {
reason: maka_agent_facts::model::error::ProviderFailureReason::Network,
},
),
class: "connection_lost".into(),
message: None,
},
},
1300,
))
.await
.unwrap();
let before = log.usage_summary(query()).await.unwrap().summary;
let frozen_before = log.usage_summary(frozen.clone()).await.unwrap().summary;
let revision = log
.get_session::<Value>("source")
.await
.unwrap()
.unwrap()
.revision;
log.copy_session(
SessionCopy {
source_session_id: "source".into(),
target_session_id: "copy".into(),
expected_source_revision: revision,
purpose: CopyPurpose::Branch {
turn_id: None,
side_conversation: false,
},
},
&json!({}),
2,
)
.await
.unwrap();
let inherited = log
.scoped_prefix(LogScope::Session { id: "copy".into() }, 100, 1_000_000)
.await
.unwrap()
.digest;
retire(&log, "source").await;
assert_eq!(
log.collect_session_material(None).await.unwrap(),
maka_event_log::sessions::MaterialCollection::Retained("source".into())
);
assert_eq!(
log.scoped_prefix(LogScope::Session { id: "copy".into() }, 100, 1_000_000)
.await
.unwrap()
.digest,
inherited
);
retire(&log, "copy").await;
let inspect = rusqlite::Connection::open(&path).unwrap();
let pages_before: i64 = inspect
.query_row("PRAGMA page_count", [], |row| row.get(0))
.unwrap();
drop(inspect);
assert_eq!(
log.collect_session_material(None).await.unwrap(),
maka_event_log::sessions::MaterialCollection::Collected
);
assert_eq!(
log.usage_summary(query()).await.unwrap().summary,
before,
"partial collection keeps accounting exact"
);
log.close().await.unwrap();
let log = EventLog::open(&path).await.unwrap();
while log.collect_session_material(None).await.unwrap()
== maka_event_log::sessions::MaterialCollection::Collected
{}
assert_eq!(log.usage_summary(query()).await.unwrap().summary, before);
assert_eq!(
log.usage_summary(frozen).await.unwrap().summary,
frozen_before
);
assert!(matches!(
log.prefix(100, 1_000_000).await,
Err(StoreError::MaterialCollected)
));
assert!(log.unfinished_invocations(10).await.unwrap().is_empty());
assert!(
log.append(&opening).await.is_err(),
"collection cannot resurrect a retired invocation"
);
assert!(log.session_copy_receipt("copy").await.unwrap().is_some());
let inspect = rusqlite::Connection::open(&path).unwrap();
let (bodies, leaked, digest): (i64, i64, String) = inspect
.query_row(
"SELECT (SELECT COUNT(*) FROM event_log WHERE event_json IS NOT NULL),
(SELECT COUNT(*) FROM event_log WHERE retained_json LIKE '%private-%'),
(SELECT body_digest FROM event_log WHERE event_id=?1)",
[&opening.event().id],
|row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
)
.unwrap();
assert_eq!((bodies, leaked), (0, 0));
let retained: Vec<String> = inspect.prepare(
"SELECT json_extract(retained_json, CASE kind WHEN 'model_interrupted' THEN '$.fact.model_failure' ELSE '$.fact.outcome.model_failure' END)
FROM event_log WHERE kind IN ('model_interrupted','invocation_ended') ORDER BY sequence"
).unwrap().query_map([], |row| row.get(0)).unwrap().collect::<Result<_,_>>().unwrap();
assert_eq!(
retained
.iter()
.map(|value| serde_json::from_str::<Value>(value).unwrap())
.collect::<Vec<_>>(),
vec![
json!({"kind":"provider","reason":"network"}),
json!({"kind":"provider","reason":"network"})
]
);
let pages_after: i64 = inspect
.query_row("PRAGMA page_count", [], |row| row.get(0))
.unwrap();
assert!(
pages_after < pages_before,
"collection returns database pages, not only reusable slots"
);
assert_eq!(
digest,
maka_agent_facts::artifact::content_digest(&serde_json::to_vec(opening.event()).unwrap())
);
log.close().await.unwrap();
}
async fn retire(log: &EventLog, session: &str) {
let revision = log
.get_session::<Value>(session)
.await
.unwrap()
.unwrap()
.revision;
log.begin_session_removal(session, revision).await.unwrap();
assert_eq!(
log.finish_session_retirement(session).await.unwrap(),
SessionRetirement::Removed
);
}