blob: 3969a6967b018532db62a34a3872b07cd316b67e [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::read::{row_position, sql_position};
use crate::{EventLog, StoreError, sequence_number, sessions};
use maka_agent_facts::{configuration::validation::MAX_SAFE_INTEGER, history::RowPosition};
use maka_presentation::navigation::{
RecordedTurnState, TurnContribution, TurnLandmark, TurnStateMessage, truncate,
};
use sqlx::{Connection, Row};
use std::collections::HashMap;
mod queries;
pub struct TurnPage {
pub contributions: Vec<TurnContribution>,
pub next_position: Option<RowPosition>,
}
impl EventLog {
/// Visible raw history fence, including running Turns, captured with Session existence.
pub async fn navigation_fence(&self, session: &str) -> Result<Option<u64>, StoreError> {
self.validate_root()?;
sessions::validate_id(session)?;
let session = session.to_owned();
self.connection
.run(move |connection| {
Box::pin(async move {
let mut tx = connection.begin().await?;
let exists: bool = sqlx::query_scalar(
"SELECT EXISTS(SELECT 1 FROM session_control WHERE id = ?)",
)
.bind(&session)
.fetch_one(&mut *tx)
.await?;
if !exists {
return Err(StoreError::SessionNotFound);
}
let raw: Option<i64> = sqlx::query_scalar(queries::FENCE)
.bind(&session)
.fetch_one(&mut *tx)
.await?;
raw.map(sequence_number).transpose()
})
})
.await
}
/// A bounded message-index walk. Contributions may split a logical Turn;
/// the original client merges them by Turn ID and recorded-state position.
pub async fn navigation_turns(
&self,
session: &str,
through: u64,
position: Option<RowPosition>,
limit: usize,
) -> Result<TurnPage, StoreError> {
self.validate_root()?;
sessions::validate_id(session)?;
let through_sql = super::read::sql_number(through)?;
let position = position.map(sql_position).transpose()?;
if !(1..=128).contains(&limit) {
return Err(invalid("invalid contribution limit"));
}
let session = session.to_owned();
self.connection
.run(move |connection| {
super::bodycache::run(connection, move |tx| Box::pin(async move {
super::directory::check_read(tx, &session, through).await?;
let rows = sqlx::query(queries::ROWS)
.bind(&session)
.bind(through_sql)
.bind(position.map(|p| p.0))
.bind(position.map(|p| p.1))
.fetch_all(&mut *tx)
.await?;
let mut contributions: Vec<TurnContribution> = Vec::new();
let mut indices = HashMap::new();
let mut next_position = None;
// Charge the full envelope, including the largest serialized pair metadata.
let envelope = serde_json::json!({
"sessionId": session, "throughSequence": through, "contributions": [],
"nextPosition": { "sourceSequence": MAX_SAFE_INTEGER, "itemIndex": MAX_SAFE_INTEGER }
});
let budget = (192_usize * 1024).saturating_sub(serde_json::to_vec(&envelope)?.len());
let mut bytes = 0usize;
for (index, row) in rows.into_iter().enumerate() {
let position = row_position(row.try_get(0)?, row.try_get(1)?)?;
let turn: String = row.try_get(2)?;
if index == 256
|| !indices.contains_key(&turn) && contributions.len() == limit
{
next_position = Some(position);
break;
}
let previous = indices.get(&turn).copied();
let (mut contribution, old_bytes) = match previous {
Some(index) => {
let old: &TurnContribution = &contributions[index];
(old.clone(), serde_json::to_vec(old)?.len() + 1)
}
None => (
TurnContribution {
turn_id: turn.clone(),
first_position: position,
latest_state: None,
user_prompt_preview: None,
},
0,
),
};
if contribution.user_prompt_preview.is_none() {
let role: String = row.try_get(3)?;
let kind: String = row.try_get(4)?;
if role == "user" && matches!(kind.as_str(), "invocation_opened" | "message_steered") {
contribution.user_prompt_preview = preview(tx,&session,position,256).await?;
}
}
if row.try_get::<String,_>(3)? == "turn_state" {
super::bodycache::ensure(tx,&session,position).await?;
let bytes: Vec<u8> = sqlx::query_scalar("SELECT payload FROM transcript_cache.rows WHERE session_id=?1 AND source_sequence=?2 AND item_index=?3")
.bind(&session).bind(sql_position(position)?.0).bind(sql_position(position)?.1).fetch_one(&mut *tx).await?;
let mut message: TurnStateMessage = serde_json::from_slice(&bytes)?;
if message.turn_id != contribution.turn_id {
return Err(StoreError::TranscriptConflict);
}
message.abort_source =
message.abort_source.map(|s| truncate(&s, 128).to_owned());
message.error_class =
message.error_class.map(|s| truncate(&s, 128).to_owned());
contribution.latest_state =
Some(RecordedTurnState { position, message });
}
let updated_bytes =
bytes - old_bytes + serde_json::to_vec(&contribution)?.len() + 1;
if !contributions.is_empty() && updated_bytes > budget {
next_position = Some(position);
break;
}
bytes = updated_bytes;
if let Some(index) = previous {
contributions[index] = contribution;
} else {
indices.insert(turn, contributions.len());
contributions.push(contribution);
}
}
Ok(TurnPage {
contributions,
next_position,
})
}))
})
.await
}
/// Rank unique Turn openings in SQL; load only selected labels and row bounds.
pub async fn navigation_landmarks(
&self,
session: &str,
through: u64,
limit: usize,
turn: Option<&str>,
) -> Result<Vec<TurnLandmark>, StoreError> {
self.validate_root()?;
sessions::validate_id(session)?;
let through_sql = super::read::sql_number(through)?;
if let Some(turn) = turn {
sessions::validate_id(turn)?;
}
let turn = turn.map(str::to_owned);
if !(1..=64).contains(&limit) {
return Err(invalid("invalid landmark limit"));
}
let session = session.to_owned();
self.connection
.run(move |connection| {
super::bodycache::run(connection, move |tx| Box::pin(async move {
super::directory::check_read(tx, &session, through).await?;
if let Some(turn) = turn {
let bounds: Option<(i64, i64, i64, i64)> = sqlx::query_as(queries::BOUNDS)
.bind(&session).bind(&turn).bind(through_sql).fetch_optional(&mut *tx).await?;
let Some((first_source, first_index, last_source, last_index)) = bounds else { return Ok(vec![]); };
let prompt: Option<(i64,i64)> = sqlx::query_as(
"SELECT row.source_sequence,row.item_index FROM transcript_directory row JOIN runtime_events source ON source.sequence=row.source_sequence
WHERE row.session_id=?1 AND row.turn_id=?2 AND row.source_sequence<=?3 AND row.role='user'
AND source.kind IN ('invocation_opened','message_steered') ORDER BY row.source_sequence,row.item_index LIMIT 1")
.bind(&session).bind(&turn).bind(through_sql).fetch_optional(&mut *tx).await?;
let label = if let Some((source,item)) = prompt { preview(tx,&session,row_position(source,item)?,96).await? } else { None };
return Ok(vec![TurnLandmark {
turn_id: turn, position: row_position(first_source, first_index)?, last_position: row_position(last_source, last_index)?,
label: label.unwrap_or_default(),
}]);
}
let selected: Vec<String> = sqlx::query_scalar(queries::LANDMARKS)
.bind(&session)
.bind(through_sql)
.bind(limit as i64)
.fetch_all(&mut *tx)
.await?;
let mut result = Vec::new();
for invocation in selected {
if let Some(row) = sqlx::query(queries::PROMPT)
.bind(&session)
.bind(&invocation)
.bind(through_sql)
.fetch_optional(&mut *tx)
.await?
{
let label = preview(tx,&session,row_position(row.try_get(1)?,row.try_get(2)?)?,96).await?;
if let Some(label) = label {
let turn_id: String = row.try_get(0)?;
let (first_source, first_index, last_source, last_index): (i64, i64, i64, i64) = sqlx::query_as(queries::BOUNDS)
.bind(&session).bind(&turn_id).bind(through_sql).fetch_one(&mut *tx).await?;
result.push(TurnLandmark {
position: row_position(first_source, first_index)?,
last_position: row_position(last_source, last_index)?,
turn_id,
label,
});
}
}
}
Ok(result)
}))
})
.await
}
}
async fn preview(
db: &mut sqlx::SqliteConnection,
session: &str,
position: RowPosition,
budget: i64,
) -> Result<Option<String>, StoreError> {
super::bodycache::ensure(db, session, position).await?;
sqlx::query_scalar("SELECT navigation_preview(COALESCE(json_extract(payload,'$.displayText'),json_extract(payload,'$.text')),?4) FROM transcript_cache.rows WHERE session_id=?1 AND source_sequence=?2 AND item_index=?3")
.bind(session).bind(sql_position(position)?.0).bind(sql_position(position)?.1).bind(budget).fetch_one(db).await.map_err(Into::into)
}
/// SQLite borrows the selected string; only the bounded preview is allocated in Rust.
pub(crate) fn register(connection: &rusqlite::Connection) -> Result<(), StoreError> {
use rusqlite::functions::FunctionFlags;
connection.create_scalar_function(
"navigation_preview",
2,
FunctionFlags::SQLITE_UTF8 | FunctionFlags::SQLITE_DETERMINISTIC,
|ctx| {
let text = ctx.get_raw(0).as_str()?;
let bytes =
usize::try_from(ctx.get::<i64>(1)?).map_err(|_| rusqlite::Error::InvalidQuery)?;
let text = maka_presentation::navigation::prompt_preview(text, bytes);
Ok((!text.is_empty()).then(|| text.to_owned()))
},
)?;
Ok(())
}
fn invalid(message: &str) -> StoreError {
StoreError::InvalidTransition(message.into())
}