blob: e24e22c9bf2b0610a75b5ab02d50f9149b66f550 [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, sequence_number, sessions, turns};
use futures_util::TryStreamExt;
use maka_agent_facts::event::RuntimeEvent;
use maka_agent_facts::event::StoredEvent;
use maka_agent_facts::interaction::InteractionRecord;
use serde::de::DeserializeOwned;
use sqlx::{Connection, Row};
mod delivery;
mod message;
pub use message::{AssistantExcerpt, MessageObservation, WorkOrigin};
mod streams;
mod tools;
pub use delivery::{StoreStreamEvent, StreamEventPage, StreamFact, ToolSettlement};
pub use streams::AssistantStreamSeed;
pub use tools::ToolStreamSeed;
/// Metadata, latest root Turn and catch-up cursor from the same SQL snapshot.
pub struct SessionObservation<T> {
pub session: sessions::SessionRecord<T>,
pub root_turn: Option<turns::TurnBoundary>,
pub through_sequence: u64,
pub active_streams: Vec<AssistantStreamSeed>,
pub active_tools: Vec<ToolStreamSeed>,
pub compacting: bool,
pub pending_interactions: Vec<InteractionRecord>,
}
/// Projection-only read: no stream offset aggregation on token wakeups.
pub struct SessionProjection<T> {
pub session: sessions::SessionRecord<T>,
pub root_turn: Option<turns::TurnBoundary>,
pub through_sequence: u64,
pub pending_interactions: Vec<InteractionRecord>,
}
/// Cheap invalidation basis for an observer, not a delivery or replay cursor.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ObservationVersion {
pub metadata: u64,
pub queue: u64,
pub event: u64,
}
/// Observation pages are ordered delivery material, not model replay proofs.
pub struct EventPage {
pub events: Vec<StoredEvent>,
pub through_sequence: u64,
/// Continue strictly after this sequence; None means the fence is exhausted.
pub next_after: Option<u64>,
}
impl EventLog {
/// Exact, bounded lookup; both Session and logical Turn are authority filters.
pub async fn execution_event(
&self,
invocation: &maka_agent_facts::event::Invocation,
event_id: &str,
through: u64,
) -> Result<Option<StoredEvent>, StoreError> {
self.validate_root()?;
sessions::validate_id(event_id)?;
if through > i64::MAX as u64 {
return Err(invalid("invalid event fence"));
}
let invocation = invocation.clone();
let event_id = event_id.to_owned();
self.connection.read(move |connection| Box::pin(async move {
let row: Option<(i64, String)> = sqlx::query_as(
"SELECT sequence, event_json FROM runtime_events WHERE event_id = ?1 AND sequence <= ?2
AND (event_json IS NOT NULL AND event_session=?3)
AND json_extract(event_json, '$.invocation.turn_id') = ?4"
).bind(event_id).bind(through as i64).bind(invocation.session_id).bind(invocation.turn_id)
.fetch_optional(connection).await?;
row.map(|(sequence, json)| Ok(StoredEvent {
sequence: sequence_number(sequence)?, event: serde_json::from_str(&json)?,
})).transpose()
})).await
}
pub async fn observation_versions(
&self,
sessions: &[String],
) -> Result<std::collections::BTreeMap<String, ObservationVersion>, StoreError> {
self.validate_root()?;
for id in sessions {
crate::sessions::validate_id(id)?;
}
let sessions = serde_json::to_string(sessions)?;
self.connection.read(move |connection| Box::pin(async move {
let rows: Vec<(String, i64, i64, i64)> = sqlx::query_as(
"SELECT session.id, session.revision,
COALESCE((SELECT revision FROM message_queue_state WHERE session_id = session.id), 0),
COALESCE((SELECT sequence FROM event_log INDEXED BY event_material_owner
WHERE invocation_id IS NOT NULL
AND (event_json IS NOT NULL AND event_session=session.id)
ORDER BY sequence DESC LIMIT 1), 0)
FROM session_control session WHERE session.id IN (SELECT value FROM json_each(?))"
).bind(sessions).fetch_all(connection).await?;
rows.into_iter().map(|(id, metadata, queue, event)| Ok((id, ObservationVersion {
metadata: sequence_number(metadata)?,
queue: sequence_number(queue)?,
event: sequence_number(event)?,
}))).collect()
})).await
}
pub async fn session_projection<T: DeserializeOwned + Send + 'static>(
&self,
session_id: &str,
) -> Result<Option<SessionProjection<T>>, StoreError> {
self.validate_root()?;
sessions::validate_id(session_id)?;
let session_id = session_id.to_owned();
self.connection
.read(move |connection| {
Box::pin(async move {
let mut transaction = connection.begin().await?;
let result = read_projection(&mut transaction, &session_id).await?;
transaction.commit().await?;
Ok(result)
})
})
.await
}
pub async fn observe_session<T: DeserializeOwned + Send + 'static>(
&self,
session_id: &str,
) -> Result<Option<SessionObservation<T>>, StoreError> {
self.validate_root()?;
sessions::validate_id(session_id)?;
let session_id = session_id.to_owned();
self.connection
.read(move |connection| {
Box::pin(async move {
let mut transaction = connection.begin().await?;
let Some(SessionProjection {
session,
root_turn,
through_sequence,
pending_interactions,
}) = read_projection(&mut transaction, &session_id).await?
else {
return Ok(None);
};
let active_streams =
streams::read(&mut transaction, root_turn.as_ref()).await?;
let active_tools = tools::read(&mut transaction, root_turn.as_ref()).await?;
let compacting =
streams::compacting(&mut transaction, root_turn.as_ref()).await?;
transaction.commit().await?;
Ok(Some(SessionObservation {
session,
root_turn,
through_sequence,
active_streams,
active_tools,
compacting,
pending_interactions,
}))
})
})
.await
}
/// Subscribe to commit wakeups before taking the initial fence. Catch-up
/// consumes only (after, through]; later commits cannot change this page.
pub async fn session_events(
&self,
session_id: &str,
after: u64,
through: u64,
max_events: usize,
max_bytes: usize,
) -> Result<EventPage, StoreError> {
self.validate_root()?;
sessions::validate_id(session_id)?;
if after > through || max_events == 0 || max_events > 512 || max_bytes == 0 {
return Err(invalid("invalid observation page bounds"));
}
let after_sql = i64::try_from(after).map_err(|_| invalid("invalid observation cursor"))?;
let through_sql =
i64::try_from(through).map_err(|_| invalid("invalid observation fence"))?;
let session_id = session_id.to_owned();
self.connection
.read(move |connection| {
Box::pin(async move {
let mut transaction = connection.begin().await?;
if through > high_water(&mut transaction).await? {
return Err(invalid("observation fence is beyond committed history"));
}
let mut events: Vec<StoredEvent> = Vec::new();
let mut bytes = 0usize;
let mut next_after = None;
{
let mut rows = sqlx::query(
"SELECT sequence, length(CAST(event_json AS BLOB)), event_json FROM runtime_events
WHERE (event_json IS NOT NULL AND event_session=?1)
AND sequence > ?2 AND sequence <= ?3 ORDER BY sequence LIMIT ?4",
)
.bind(&session_id)
.bind(after_sql)
.bind(through_sql)
.bind(max_events as i64 + 1)
.fetch(&mut *transaction);
while let Some(row) = rows.try_next().await? {
let size = usize::try_from(row.try_get::<i64, _>(1)?)
.map_err(|_| StoreError::PrefixTooLarge)?;
if events.len() == max_events || (!events.is_empty() && size > max_bytes.saturating_sub(bytes))
{
next_after = events.last().map(|event| event.sequence);
break;
}
let sequence = sequence_number(row.try_get(0)?)?;
let event: RuntimeEvent =
serde_json::from_str(row.try_get::<&str, _>(2)?)?;
if event.invocation.session_id != session_id {
return Err(invalid("stored observation scope changed"));
}
bytes += size;
events.push(StoredEvent { sequence, event });
}
}
transaction.commit().await?;
Ok(EventPage {
events,
through_sequence: through,
next_after,
})
})
})
.await
}
}
async fn read_projection<T: DeserializeOwned + Send>(
connection: &mut sqlx::SqliteConnection,
session_id: &str,
) -> Result<Option<SessionProjection<T>>, StoreError> {
let Some(session) = sessions::read(connection, session_id).await? else {
return Ok(None);
};
Ok(Some(SessionProjection {
session,
root_turn: turns::read(connection, session_id, None).await?,
through_sequence: high_water(connection).await?,
pending_interactions: crate::interactions::pending(connection, session_id).await?,
}))
}
pub(crate) async fn high_water(connection: &mut sqlx::SqliteConnection) -> Result<u64, StoreError> {
sequence_number(
sqlx::query_scalar("SELECT COALESCE(MAX(sequence), 0) FROM event_log")
.fetch_one(connection)
.await?,
)
}
fn invalid(message: &str) -> StoreError {
StoreError::InvalidTransition(message.into())
}