blob: 7ede24ecb5d344951df1cd20fa0178d1724aa997 [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 super::format::Record;
use crate::{
StoreError,
context::{self, selection::Selection},
};
use maka_agent_facts::context::CheckpointMode;
use maka_agent_facts::context::ModelPurpose;
use maka_agent_facts::event::EventWrite;
use maka_agent_facts::event::Fact;
use maka_agent_facts::event::RuntimeEvent;
use maka_agent_facts::event::ToolOutcome;
use sqlx::{Connection, SqliteConnection, sqlite::SqliteConnectOptions};
mod artifacts;
mod catalog;
mod copies;
mod material;
pub(super) async fn validate(staged: &mut SqliteConnection) -> Result<(), StoreError> {
prepare(staged, None).await?.close().await?;
Ok(())
}
pub(super) async fn prepare(
staged: &mut SqliteConnection,
positions: Option<&super::relocation::Positions>,
) -> Result<SqliteConnection, StoreError> {
let mut original = SqliteConnection::connect_with(
&SqliteConnectOptions::new()
.filename("")
.create_if_missing(true)
.pragma("temp_store", "FILE")
.pragma("page_size", "4096")
.pragma("cache_size", "-4096")
.pragma("max_page_count", "393216"),
)
.await?;
let result = async {
crate::schema::initialize_connection(&mut original).await?;
let mut tx = original.begin().await?;
replay(staged, &mut tx, positions).await?;
tx.commit().await?;
Ok::<_, StoreError>(())
}
.await;
match result {
Ok(()) => Ok(original),
Err(error) => {
original.close().await?;
Err(error)
}
}
}
async fn replay(
staged: &mut SqliteConnection,
original: &mut SqliteConnection,
positions: Option<&super::relocation::Positions>,
) -> Result<(), StoreError> {
let header: String = sqlx::query_scalar("SELECT record_json FROM frames WHERE number=1")
.fetch_one(&mut *staged)
.await?;
let Record::Header {
source_high_water,
inventory,
} = serde_json::from_str(&header)?
else {
return Err(invalid("missing staged header"));
};
let mut copies = copies::Copies::read(staged, source_high_water, positions).await?;
let source_high_water = positions.map_or(source_high_water, |p| p.fence(source_high_water));
let mut after = 0i64;
loop {
let row: Option<String> = sqlx::query_scalar(
"SELECT record_json FROM frames INDEXED BY source_events WHERE kind='event' AND source_sequence>? ORDER BY source_sequence LIMIT 1"
).bind(after).fetch_optional(&mut *staged).await?;
let Some(row) = row else {
break;
};
let Record::Event { sequence, json } = serde_json::from_str(&row)? else {
unreachable!()
};
after = sequence as i64;
let sequence = positions.map_or(Ok(sequence), |p| p.event(sequence))?;
copies
.install(staged, original, sequence - 1, positions)
.await?;
let json = match positions {
Some(positions) => positions.json(original, sequence, json).await?,
None => json,
};
let event: RuntimeEvent = serde_json::from_str(&json)?;
for id in [
&event.id,
&event.invocation.session_id,
&event.invocation.turn_id,
&event.invocation.run_id,
&event.invocation.invocation_id,
] {
crate::sessions::validate_id(id)?;
}
let payload = material::tool(staged, &event).await?;
crate::tool_payloads::verify_binding(&event, payload.as_ref().map(|p| p.len() as i64))?;
match &event.fact {
Fact::MessageImported { source, record } => {
source.validate().map_err(invalid)?;
record.validate().map_err(invalid)?;
}
_ => {
if !matches!(
event.fact,
Fact::ToolSettled {
outcome: ToolOutcome::Succeeded { .. },
..
}
) {
EventWrite::plain(event.clone()).map_err(|e| invalid(&e.to_string()))?;
}
crate::append::history::validate(original, &event).await?;
request(original, &event, sequence).await?;
}
}
crate::message_identity::validate(original, &event).await?;
crate::message_identity::validate_source_id(
original,
&event.invocation.session_id,
&event.id,
)
.await?;
sqlx::query("INSERT INTO event_log(sequence,event_id,invocation_id,kind,operation_id,event_json) VALUES(?,?,?,?,?,?)")
.bind(sequence as i64).bind(&event.id).bind(&event.invocation.invocation_id)
.bind(event.fact.kind()).bind(event.fact.operation_id()).bind(json)
.execute(&mut *original).await?;
crate::message_sources::insert(original, &event).await?;
if let Some(payload) = payload {
sqlx::query("INSERT INTO tool_result_payloads(event_id,payload) VALUES(?,?)")
.bind(&event.id)
.bind(payload)
.execute(&mut *original)
.await?;
}
material::composition(staged, original, &event).await?;
}
// The fence can fall in a gap, or after the last exported proof event.
copies
.install(staged, original, source_high_water, positions)
.await?;
let mixed: bool = sqlx::query_scalar(
"SELECT EXISTS(SELECT 1 FROM runtime_events GROUP BY invocation_id
HAVING SUM(kind='message_imported')>0 AND COUNT(*)>1)
OR EXISTS(SELECT 1 FROM runtime_events GROUP BY event_session,json_extract(event_json,'$.invocation.run_id')
HAVING COUNT(DISTINCT invocation_id)>1)",
)
.fetch_one(&mut *original)
.await?;
if mixed {
return Err(invalid("imported messages cannot admit an invocation"));
}
let mut after = 0i64;
loop {
let row: Option<(i64,String)> = sqlx::query_as(
"SELECT sequence,event_json FROM runtime_events WHERE kind='context_checkpoint_recorded' AND sequence>? ORDER BY sequence LIMIT 1"
).bind(after).fetch_optional(&mut *original).await?;
let Some((sequence, json)) = row else {
break;
};
let event: RuntimeEvent = serde_json::from_str(&json)?;
if matches!(&event.fact, Fact::ContextCheckpointRecorded { checkpoint } if matches!(checkpoint.mode, CheckpointMode::Standalone))
{
context::validate_checkpoint_terminal(original, &event).await?;
}
after = sequence;
}
material::check_bindings(staged, original).await?;
catalog::validate(staged, original, &inventory, source_high_water).await?;
artifacts::validate(staged, original, &inventory, positions).await?;
super::accounting::validate(staged, original).await
}
async fn request(
db: &mut SqliteConnection,
event: &RuntimeEvent,
sequence: u64,
) -> Result<(), StoreError> {
let Fact::ModelRequested {
source_scope,
source_high_water,
source_digest,
checkpoint_event_id,
effective_source_digest,
purpose,
..
} = &event.fact
else {
return Ok(());
};
let selection = Selection::for_invocation(db, &event.invocation).await?;
if *purpose == ModelPurpose::Main {
context::safety::model_source_unchanged(db, &selection, event).await?;
}
if *source_high_water >= sequence || *source_scope != selection.scope {
return Err(invalid(
"request source is not its preceding selected history",
));
}
let source = context::evidence::selected(db, &selection, *source_high_water).await?;
if source.digest != *source_digest {
return Err(invalid("request original source digest mismatch"));
}
let baseline = context::read::latest_selected(
db,
&selection,
*source_high_water,
&mut context::cache::Verification::default(),
)
.await?;
if baseline.as_ref().map(|b| &b.event_id) != checkpoint_event_id.as_ref() {
return Err(invalid("request original baseline mismatch"));
}
let effective = effective_source_digest
.as_deref()
.ok_or_else(|| invalid("request lacks effective source evidence"))?;
crate::archive::validate_summary(db, &source, effective, sequence).await
}
fn invalid(message: &str) -> StoreError {
StoreError::InvalidTransition(message.into())
}