blob: 5c4c31873f50e7f9863219c9c368fca106ef00eb [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, message_resolution::MessageExecution};
use sqlx::{Connection, SqliteConnection};
pub struct AssistantExcerpt {
pub text: String,
pub complete: bool,
}
/// Exact delivery lineage and its terminal answer from one read snapshot.
pub struct MessageObservation {
pub execution: MessageExecution,
pub answer: Option<AssistantExcerpt>,
/// Exact Run-local count from the same snapshot as delivery and answer.
pub high_water: u64,
pub interactions: Vec<maka_agent_facts::interaction::InteractionRecord>,
}
/// Durable work origin, independent of a Session's current tip.
pub enum WorkOrigin {
Message(String),
Invocation(String),
}
impl EventLog {
pub async fn message_observation(
&self,
session: &str,
message: &str,
offset: u64,
) -> Result<MessageObservation, StoreError> {
self.work_observation(session, WorkOrigin::Message(message.into()), offset, None)
.await
}
/// Delivery lineage, original answer and Run-local boundary share one snapshot.
pub async fn work_observation(
&self,
session: &str,
origin: WorkOrigin,
offset: u64,
answer_invocation: Option<&str>,
) -> Result<MessageObservation, StoreError> {
self.validate_root()?;
crate::sessions::validate_id(session)?;
match &origin {
WorkOrigin::Message(id) | WorkOrigin::Invocation(id) => {
crate::sessions::validate_id(id)?
}
}
if let Some(id) = answer_invocation {
crate::sessions::validate_id(id)?;
}
if offset > 9_007_199_254_740_991 {
return Err(StoreError::InvalidTransition(
"invalid answer offset".into(),
));
}
let session = session.to_owned();
let answer_invocation = answer_invocation.map(str::to_owned);
self.connection
.run(move |connection| {
Box::pin(async move {
let mut tx = connection.begin().await?;
let execution = match origin {
WorkOrigin::Message(message) => {
crate::message_resolution::owner::execution(&mut tx, &session, &message)
.await?
}
WorkOrigin::Invocation(invocation) => {
let opening: Option<String> = sqlx::query_scalar(
"SELECT event_json
FROM runtime_events WHERE invocation_id = ?1 AND kind = 'invocation_opened'
AND json_extract(event_json, '$.invocation.session_id') = ?2",
)
.bind(invocation)
.bind(&session)
.fetch_optional(&mut *tx)
.await?;
match opening {
None => MessageExecution::Missing,
Some(opening) => {
let opening: maka_agent_facts::event::RuntimeEvent =
serde_json::from_str(&opening)?;
MessageExecution::Owned(Box::new(
crate::message_resolution::owner::lineage(
&mut tx,
&opening.invocation,
)
.await?,
))
}
}
}
};
observe(
&mut tx,
&session,
execution,
offset,
answer_invocation.as_deref(),
)
.await
})
})
.await
}
}
async fn observe(
tx: &mut SqliteConnection,
session: &str,
execution: MessageExecution,
offset: u64,
answer_invocation: Option<&str>,
) -> Result<MessageObservation, StoreError> {
let answer = match &execution {
MessageExecution::Owned(boundary) | MessageExecution::Shared(boundary)
if boundary.state.terminal_outcome().is_some()
&& answer_invocation.is_none_or(|id| id == boundary.invocation.invocation_id) =>
{
// Do not hydrate model/tool bodies or scan another Run's output.
// Blob slicing preserves NUL and bounds bytes, not characters.
let page: Option<(Vec<u8>, i64)> = sqlx::query_as(
"SELECT substr(CAST(text AS BLOB), ?2, 16388), length(CAST(text AS BLOB)) FROM (
SELECT sequence, json_extract(event_json, '$.fact.text') AS text
FROM runtime_events WHERE invocation_id = ?1 AND kind = 'executor_completed'
UNION ALL
SELECT event.sequence, (
SELECT group_concat(json_extract(part.value, '$.text'), '')
FROM json_each(event.event_json, '$.fact.output.parts') part
WHERE json_extract(part.value, '$.kind') = 'text'
AND json_extract(part.value, '$.text_kind') = 'text'
) AS text
FROM runtime_events event WHERE invocation_id = ?1 AND kind = 'model_completed'
) WHERE text IS NOT NULL AND trim(text) != ''
ORDER BY sequence DESC LIMIT 1"
).bind(&boundary.invocation.invocation_id).bind(offset as i64 + 1).fetch_optional(&mut *tx).await?;
page.map(|(bytes, total)| {
if offset > total as u64 {
return Err(StoreError::InvalidTransition(
"answer cursor exceeds answer length".into(),
));
}
let text = match std::str::from_utf8(&bytes) {
Ok(text) => text,
Err(error) if error.error_len().is_none() && error.valid_up_to() > 0 => {
std::str::from_utf8(&bytes[..error.valid_up_to()])
.expect("validated UTF-8 prefix")
}
Err(_) => {
return Err(StoreError::InvalidTransition(
"answer cursor is not a UTF-8 boundary".into(),
));
}
};
let end = text.floor_char_boundary(text.len().min(16384));
Ok(AssistantExcerpt {
text: text[..end].into(),
complete: offset + end as u64 >= total as u64,
})
})
.transpose()?
}
_ => None,
};
let interactions = match &execution {
MessageExecution::Owned(boundary) | MessageExecution::Shared(boundary)
if boundary.state.terminal_outcome().is_none() =>
{
crate::interactions::pending(tx, session)
.await?
.into_iter()
.filter(|record| {
record.turn_id == boundary.invocation.turn_id
&& record.run_id == boundary.invocation.run_id
})
.collect()
}
_ => Vec::new(),
};
let high_water = match &execution {
MessageExecution::Owned(boundary) | MessageExecution::Shared(boundary) => {
let count: i64 =
sqlx::query_scalar("SELECT COUNT(*) FROM runtime_events WHERE invocation_id = ?")
.bind(&boundary.invocation.invocation_id)
.fetch_one(&mut *tx)
.await?;
count as u64
}
_ => 0,
};
Ok(MessageObservation {
execution,
answer,
interactions,
high_water,
})
}