blob: a1a23bc0bba81c09020ed2f726777658fd69a969 [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::Inventory;
use crate::{StoreError, context::selection::Selection, sequence_number};
use maka_agent_facts::context::CheckpointMode;
use maka_agent_facts::event::Fact;
use maka_agent_facts::event::LogScope;
use maka_agent_facts::event::RuntimeEvent;
use sqlx::SqliteConnection;
use std::collections::{BTreeMap, BTreeSet, HashMap, VecDeque};
mod members;
/// Event identities and exact copied memberships, not additional catalog Sessions.
pub(super) struct Closure {
pub events: BTreeSet<u64>,
pub members: BTreeMap<(String, u64), Option<u64>>,
pub copies: BTreeSet<String>,
pub revisions: BTreeSet<(String, u64)>,
queue: VecDeque<u64>,
scopes: HashMap<String, u64>,
runs: HashMap<String, u64>,
}
impl Closure {
pub async fn capture(
db: &mut SqliteConnection,
inventory: &Inventory,
through: u64,
) -> Result<Self, StoreError> {
let mut selected = Self {
events: BTreeSet::new(),
members: BTreeMap::new(),
copies: BTreeSet::new(),
revisions: BTreeSet::new(),
queue: VecDeque::new(),
scopes: HashMap::new(),
runs: HashMap::new(),
};
for session in &inventory.sessions {
selected
.scope(
db,
&LogScope::Session {
id: session.id.clone(),
},
through,
)
.await?;
let revisions: Vec<i64> = sqlx::query_scalar(
"SELECT sequence FROM session_revision_sources WHERE session_id=? ORDER BY sequence"
).bind(&session.id).fetch_all(&mut *db).await?;
for sequence in revisions {
let sequence = sequence_number(sequence)?;
selected.add(sequence)?;
selected.revisions.insert((session.id.clone(), sequence));
let source: String = sqlx::query_scalar(
"SELECT source_session_id FROM session_history_copies WHERE session_id=?",
)
.bind(&session.id)
.fetch_one(&mut *db)
.await?;
selected.member(db, &source, sequence).await?;
}
}
while let Some(sequence) = selected.queue.pop_front() {
let row: Option<(Option<String>, Option<i64>)> = sqlx::query_as(
"SELECT event_json,
(SELECT length(payload) FROM tool_result_payloads WHERE event_id=e.event_id)
FROM runtime_events e WHERE sequence=?1",
)
.bind(sequence as i64)
.fetch_optional(&mut *db)
.await?;
let (json, payload_bytes) = row.ok_or(StoreError::MaterialCollected)?;
let json = json.ok_or(StoreError::PrefixTooLarge)?;
let event: RuntimeEvent = serde_json::from_str(&json)?;
crate::tool_payloads::verify_binding(&event, payload_bytes)?;
if !matches!(event.fact, Fact::MessageImported { .. }) {
selected
.run(db, &event.invocation.invocation_id, sequence)
.await?;
}
match &event.fact {
Fact::ModelRequested {
source_scope,
source_high_water,
..
} => {
selected.scope(db, source_scope, *source_high_water).await?;
selected
.archives(db, source_scope, *source_high_water, sequence)
.await?;
}
Fact::ContextCheckpointRecorded { checkpoint } => {
let selection = Selection::for_invocation(db, &event.invocation).await?;
selected
.scope(db, &selection.scope, checkpoint.covered_through)
.await?;
if matches!(checkpoint.mode, CheckpointMode::Standalone) {
let terminal: i64 = sqlx::query_scalar(
"SELECT sequence FROM runtime_events WHERE invocation_id=? AND kind='invocation_ended'"
).bind(&event.invocation.invocation_id).fetch_one(&mut *db).await?;
selected.add(sequence_number(terminal)?)?;
}
}
Fact::InvocationOpened { input, .. } => {
if let Some(claim) = input.inherited_claim() {
let source = &claim.source.invocation;
let end: i64 = sqlx::query_scalar(
"SELECT sequence FROM runtime_events WHERE invocation_id=? AND kind='invocation_ended'"
).bind(&source.invocation_id).fetch_one(&mut *db).await?;
selected
.run(db, &source.invocation_id, sequence_number(end)?)
.await?;
selected
.scope(
db,
&LogScope::Session {
id: source.session_id.clone(),
},
claim.base.high_water,
)
.await?;
}
}
Fact::ToolResultArchived { placeholder } => {
let target: i64 =
sqlx::query_scalar("SELECT sequence FROM runtime_events WHERE event_id=?")
.bind(&placeholder.identity.runtime_event_id)
.fetch_one(&mut *db)
.await?;
let selection = Selection::for_invocation(db, &event.invocation).await?;
selected
.scope(db, &selection.scope, sequence_number(target)?)
.await?;
}
_ => {}
}
}
Ok(selected)
}
fn add(&mut self, sequence: u64) -> Result<(), StoreError> {
if self.events.insert(sequence) {
self.queue.push_back(sequence);
}
Ok(())
}
async fn run(
&mut self,
db: &mut SqliteConnection,
invocation: &str,
through: u64,
) -> Result<(), StoreError> {
let prior = self.runs.get(invocation).copied().unwrap_or(0);
if through <= prior {
return Ok(());
}
let rows: Vec<i64> = sqlx::query_scalar(
"SELECT sequence FROM runtime_events WHERE invocation_id=? AND sequence>? AND sequence<=? ORDER BY sequence"
).bind(invocation).bind(prior as i64).bind(through as i64).fetch_all(db).await?;
for row in rows {
self.add(sequence_number(row)?)?;
}
self.runs.insert(invocation.into(), through);
Ok(())
}
async fn scope(
&mut self,
db: &mut SqliteConnection,
scope: &LogScope,
through: u64,
) -> Result<(), StoreError> {
if through > i64::MAX as u64 {
return Err(StoreError::PrefixTooLarge);
}
if matches!(scope, LogScope::Root) {
return Err(StoreError::InvalidTransition(
"Root-scoped proof cannot be exported as a Session bundle".into(),
));
}
let key = serde_json::to_string(scope)?;
let prior = self.scopes.get(&key).copied();
if prior.is_some_and(|previous| through <= previous) {
return Ok(());
}
let selection = Selection::resolve(db, scope).await?;
let session = selection.session.as_ref().expect("non-root selection");
let copy: bool = sqlx::query_scalar(
"SELECT EXISTS(SELECT 1 FROM session_history_copies WHERE session_id=?)",
)
.bind(session)
.fetch_one(&mut *db)
.await?;
if copy {
self.copies.insert(session.clone());
}
let filter = Selection::predicate("e", "?3");
let collected: bool = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
"SELECT EXISTS(SELECT 1 FROM event_log e WHERE e.event_session=?1 AND e.invocation_id IS NOT NULL
AND e.sequence<=?2 AND e.event_json IS NULL AND {filter}
UNION ALL SELECT 1 FROM session_history_members h JOIN event_log e ON e.sequence=h.sequence
WHERE h.session_id=?1 AND e.sequence<=?2 AND e.event_json IS NULL AND {filter})"
))).bind(session).bind(through as i64).bind(&selection.lineage).fetch_one(&mut *db).await?;
if collected {
return Err(StoreError::MaterialCollected);
}
let filter = Selection::predicate("e", "?4");
let rows: Vec<(i64, bool)> = sqlx::query_as(sqlx::AssertSqlSafe(format!(
"SELECT e.sequence,e.inherited FROM session_history_events e
WHERE e.owner_session_id=?1 AND e.sequence>?2 AND e.sequence<=?3 AND {filter}
ORDER BY e.sequence"
)))
.bind(session)
.bind(prior.unwrap_or(0) as i64)
.bind(through as i64)
.bind(&selection.lineage)
.fetch_all(&mut *db)
.await?;
for (sequence, inherited) in rows {
let sequence = sequence_number(sequence)?;
self.add(sequence)?;
if inherited {
self.member(db, session, sequence).await?;
}
}
self.scopes.insert(key, through);
Ok(())
}
async fn archives(
&mut self,
db: &mut SqliteConnection,
scope: &LogScope,
through: u64,
before: u64,
) -> Result<(), StoreError> {
let selection = Selection::resolve(db, scope).await?;
let filter = Selection::archive_predicate("t", "a", "?3", "?4");
let target = Selection::predicate("t", "?4");
let rows: Vec<i64> = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
"SELECT a.sequence FROM session_history_events t JOIN runtime_events a
ON a.kind='tool_result_archived' AND json_extract(a.event_json,'$.fact.placeholder.identity.runtime_event_id')=t.event_id
WHERE t.owner_session_id=?1 AND t.sequence<=?2 AND {filter} AND {target}
ORDER BY a.sequence"
))).bind(&selection.session).bind(through as i64).bind(before as i64).bind(&selection.lineage)
.fetch_all(db).await?;
for row in rows {
self.add(sequence_number(row)?)?;
}
Ok(())
}
}