blob: b4943e2504232a7629a3bca3b5cc72df79d18b8d [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::records::{encode, invalid, read};
use crate::{EventLog, StoreError};
use maka_agent_facts::event::Invocation;
use maka_agent_facts::interaction::ClosureReason;
use maka_agent_facts::interaction::InteractionOutcome;
use maka_agent_facts::interaction::entity_id;
use sqlx::{Connection, SqliteConnection};
impl EventLog {
pub async fn close_run_interactions(
&self,
invocation: &Invocation,
reason: ClosureReason,
now: u64,
) -> Result<usize, StoreError> {
validate_scope(invocation)?;
self.close_interactions(Some(invocation.clone()), reason, now)
.await
}
/// Startup only: requires exclusive Host ownership and no live producers.
/// Includes pending requests whose execution Run has already been sealed.
pub async fn close_abandoned_interactions(&self, now: u64) -> Result<usize, StoreError> {
self.close_interactions(None, ClosureReason::HostRestarted, now)
.await
}
async fn close_interactions(
&self,
scope: Option<Invocation>,
reason: ClosureReason,
now: u64,
) -> Result<usize, StoreError> {
self.validate_root()?;
let outcome = InteractionOutcome::Closure {
reason,
committed_at: now,
};
outcome.validate().map_err(invalid)?;
let commits = self.commits.clone();
self.connection
.run(move |connection| {
Box::pin(async move {
let mut tx = connection.begin_with("BEGIN IMMEDIATE").await?;
let count = visit_pending(&mut tx, scope.as_ref(), Some(&outcome)).await?;
if count > 0 {
tx.commit().await.map_err(StoreError::CommitUnknown)?;
// Interaction facts wake observers without advancing the execution cursor.
commits.send_modify(|_| {});
} else {
tx.rollback().await?;
}
Ok(count)
})
})
.await
}
}
fn validate_scope(invocation: &Invocation) -> Result<(), StoreError> {
for id in [
&invocation.session_id,
&invocation.turn_id,
&invocation.run_id,
] {
entity_id(id).map_err(invalid)?;
}
Ok(())
}
pub(crate) async fn require_closed(
connection: &mut SqliteConnection,
invocation: &Invocation,
) -> Result<(), StoreError> {
validate_scope(invocation)?;
if visit_pending(connection, Some(invocation), None).await? > 0 {
return Err(invalid("cannot seal a Run with pending interactions"));
}
Ok(())
}
/// Bound allocations independently of the number of requests in the root.
/// Decode and validate source facts before deciding Run membership or closing.
async fn visit_pending(
connection: &mut SqliteConnection,
scope: Option<&Invocation>,
closure: Option<&InteractionOutcome>,
) -> Result<usize, StoreError> {
let mut after = String::new();
let mut count = 0;
loop {
let ids: Vec<String> = sqlx::query_scalar(
"SELECT request_id FROM interaction_requests AS request
WHERE request_id > ? AND (? IS NULL OR session_id = ?)
AND NOT EXISTS (SELECT 1 FROM interaction_outcomes AS outcome
WHERE outcome.request_id = request.request_id)
ORDER BY request_id LIMIT 64",
)
.bind(&after)
.bind(scope.map(|scope| &scope.session_id))
.bind(scope.map(|scope| &scope.session_id))
.fetch_all(&mut *connection)
.await?;
if ids.is_empty() {
return Ok(count);
}
for id in &ids {
let record = read(connection, id)
.await?
.ok_or_else(|| invalid("stored interaction disappeared"))?;
if let Some(scope) = scope
&& (record.session_id != scope.session_id
|| record.turn_id != scope.turn_id
|| record.run_id != scope.run_id)
{
continue;
}
let Some(closure) = closure else { return Ok(1) };
closure
.validate_for_request(&record.request)
.map_err(invalid)?;
sqlx::query("INSERT INTO interaction_outcomes VALUES (?, ?)")
.bind(id)
.bind(encode(closure)?)
.execute(&mut *connection)
.await?;
count += 1;
crate::sessions::advance_revision(connection, &record.session_id).await?;
}
after = ids.last().expect("nonempty page").clone();
}
}