blob: 91bdb5ea244b7b6bc8d867a649e002c2c0cf0067 [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 crate::{EventLog, StoreError};
use maka_agent_facts::artifact::content_digest;
use maka_agent_facts::composition::RequestComposition;
use maka_agent_facts::event::EventWrite;
use sqlx::SqliteConnection;
pub(crate) async fn insert(
connection: &mut SqliteConnection,
write: &EventWrite,
) -> Result<(), StoreError> {
validate_retry(
connection,
write.event(),
write.composition().map(|surface| surface.digest()),
)
.await?;
if let maka_agent_facts::event::Fact::ModelRequested {
purpose: maka_agent_facts::context::ModelPurpose::Summary,
source_scope,
source_high_water,
..
} = &write.event().fact
{
let first: Option<Option<String>> = sqlx::query_scalar(
"SELECT c.digest FROM runtime_events r LEFT JOIN model_request_compositions c ON c.event_id=r.event_id
WHERE r.invocation_id=?1 AND r.kind='model_requested' AND r.event_id!=?2
AND json_extract(r.event_json,'$.fact.purpose')='summary'
AND json_extract(r.event_json,'$.fact.source_scope')=json(?3)
AND json_extract(r.event_json,'$.fact.source_high_water')=?4
ORDER BY r.sequence LIMIT 1",
).bind(&write.event().invocation.invocation_id).bind(&write.event().id)
.bind(serde_json::to_string(source_scope)?).bind(*source_high_water as i64)
.fetch_optional(&mut *connection).await?;
if first.is_some_and(|digest| {
digest.as_deref() != write.composition().map(|surface| surface.digest())
}) {
return Err(StoreError::InvalidTransition(
"summary repair changed frozen composition".into(),
));
}
}
let Some(surface) = write.composition() else {
return Ok(());
};
sqlx::query("INSERT INTO request_compositions (digest, surface) VALUES (?, ?) ON CONFLICT(digest) DO NOTHING")
.bind(surface.digest()).bind(surface.bytes()).execute(&mut *connection).await?;
// A digest is an index, not permission to disregard inconsistent stored bytes.
let identical: bool =
sqlx::query_scalar("SELECT surface = ? FROM request_compositions WHERE digest = ?")
.bind(surface.bytes())
.bind(surface.digest())
.fetch_one(&mut *connection)
.await?;
if !identical {
return Err(StoreError::EventConflict);
}
sqlx::query("INSERT INTO model_request_compositions (event_id, digest) VALUES (?, ?)")
.bind(&write.event().id)
.bind(surface.digest())
.execute(connection)
.await?;
Ok(())
}
/// Replaying a frozen Main request cannot replace its capability/model surface.
/// This runs inside live append and bundle validation, after causal validation.
pub(crate) async fn validate_retry(
connection: &mut SqliteConnection,
event: &maka_agent_facts::event::RuntimeEvent,
digest: Option<&str>,
) -> Result<(), StoreError> {
let maka_agent_facts::event::Fact::ModelRequested {
purpose: maka_agent_facts::context::ModelPurpose::Main,
source_high_water,
..
} = &event.fact
else {
return Ok(());
};
let prior: Option<Option<String>> = sqlx::query_scalar(
"SELECT c.digest FROM runtime_events r LEFT JOIN model_request_compositions c ON c.event_id=r.event_id
WHERE r.invocation_id=?1 AND r.kind='model_requested' AND r.event_id!=?2 AND r.sequence>?3
AND json_remove(json_extract(r.event_json,'$.fact'),'$.step_id')=json_remove(json(?4),'$.step_id')
ORDER BY r.sequence LIMIT 1"
).bind(&event.invocation.invocation_id).bind(&event.id).bind(*source_high_water as i64)
.bind(serde_json::to_string(&event.fact)?).fetch_optional(connection).await?;
if prior.is_some_and(|prior| prior.as_deref() != digest) {
return Err(StoreError::InvalidTransition(
"main retry changed frozen composition".into(),
));
}
Ok(())
}
pub(crate) async fn verify_replay(
connection: &mut SqliteConnection,
write: &EventWrite,
) -> Result<(), StoreError> {
let stored: Option<(String, Vec<u8>)> = sqlx::query_as(
"SELECT digest, surface FROM request_compositions JOIN model_request_compositions USING(digest) WHERE event_id = ?"
).bind(&write.event().id).fetch_optional(connection).await?;
match (stored, write.composition()) {
(None, None) => Ok(()),
(Some((digest, bytes)), Some(surface))
if digest == surface.digest() && bytes == surface.bytes() =>
{
Ok(())
}
_ => Err(StoreError::EventConflict),
}
}
impl EventLog {
/// Resolve the frozen request behind a previously validated Main completion.
pub async fn completed_request_composition(
&self,
session: &str,
sequence: u64,
) -> Result<Option<RequestComposition>, StoreError> {
self.validate_root()?;
crate::sessions::validate_id(session)?;
let owned_session = session.to_owned();
let event_id: Option<String> = self
.connection
.run(move |connection| {
Box::pin(async move {
Ok(sqlx::query_scalar(
"SELECT r.event_id FROM runtime_events c JOIN runtime_events r
ON r.invocation_id=c.invocation_id AND r.operation_id=c.operation_id
WHERE c.sequence=?1 AND c.kind='model_completed' AND r.kind='model_requested'
AND json_extract(c.event_json,'$.invocation.session_id')=?2",
)
.bind(sequence as i64)
.bind(owned_session)
.fetch_optional(connection)
.await?)
})
})
.await?;
match event_id {
Some(id) => self.request_composition(session, &id).await,
None => Ok(None),
}
}
pub async fn request_composition(
&self,
session: &str,
event_id: &str,
) -> Result<Option<RequestComposition>, StoreError> {
self.validate_root()?;
crate::sessions::validate_id(session)?;
crate::sessions::validate_id(event_id)?;
let (session, event_id) = (session.to_owned(), event_id.to_owned());
self.connection.run(move |connection| Box::pin(async move {
let row: Option<(String, Vec<u8>)> = sqlx::query_as(
"SELECT composition.digest, composition.surface FROM request_compositions AS composition
JOIN model_request_compositions AS request ON request.digest = composition.digest
JOIN runtime_events AS event ON event.event_id = request.event_id
WHERE request.event_id = ? AND json_extract(event.event_json, '$.invocation.session_id') = ?"
).bind(event_id).bind(session).fetch_optional(connection).await?;
row.map(|(digest, bytes)| {
if content_digest(&bytes) != digest { return Err(StoreError::InvalidTransition("request composition integrity mismatch".into())); }
let surface: RequestComposition = serde_json::from_slice(&bytes)?;
surface.clone().freeze().map_err(|error| StoreError::InvalidTransition(error.into()))?;
Ok(surface)
}).transpose()
})).await
}
}