blob: ae0e0969e96fbd84b38c9bfa28c8c81c1672b0a8 [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};
use maka_agent_facts::history::RowPosition;
use maka_agent_ports::session::history::Prompt;
const QUERY: &str = "SELECT source_sequence,item_index FROM transcript_directory
WHERE session_id=?1 AND (source_sequence,item_index)<=(?2,?3) AND role='user'
ORDER BY source_sequence DESC,item_index DESC LIMIT 1";
impl EventLog {
/// Inclusive reading anchor, fenced to committed directory metadata.
/// The partial index skips answer/tool bodies regardless of session size.
pub async fn history_prompt(
&self,
session: &str,
through: u64,
position: RowPosition,
) -> Result<Option<Prompt>, StoreError> {
self.validate_root()?;
crate::sessions::validate_id(session)?;
super::read::sql_number(through)?;
if position.source_sequence > through {
return Err(StoreError::InvalidTransition(
"prompt anchor exceeds history fence".into(),
));
}
let (source_sequence, item_index) = super::read::sql_position(position)?;
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 row: Option<(i64,i64)> = sqlx::query_as(QUERY)
.bind(&session).bind(source_sequence).bind(item_index)
.fetch_optional(&mut *tx).await?;
let Some((source,item)) = row else { return Ok(None) };
let position = super::read::row_position(source,item)?;
super::bodycache::ensure(tx,&session,position).await?;
let text: String = sqlx::query_scalar("SELECT COALESCE(navigation_preview(COALESCE(json_extract(payload,'$.displayText'),json_extract(payload,'$.text'),''),4096),'[attachments]') FROM transcript_cache.rows WHERE session_id=?1 AND source_sequence=?2 AND item_index=?3")
.bind(&session).bind(source).bind(item).fetch_one(&mut *tx).await?;
Ok(Some(Prompt { position,text }))
}))
})
.await
}
}
#[cfg(test)]
mod tests {
use super::*;
use rusqlite::{Connection, params};
#[test]
fn preceding_prompt_uses_index_and_does_not_read_other_sessions_or_later_prompts() {
let db = Connection::open_in_memory().unwrap();
super::super::navigation::register(&db).unwrap();
db.execute_batch(
"CREATE TABLE transcript_directory(session_id TEXT, source_sequence INTEGER, item_index INTEGER, role TEXT);",
)
.unwrap();
db.execute_batch("CREATE INDEX directory_user ON transcript_directory (session_id, source_sequence, item_index) WHERE role = 'user';").unwrap();
for (session, sequence, role) in [
("a", 1, "user"),
("a", 2, "assistant"),
("b", 3, "user"),
("a", 4, "user"),
] {
db.execute(
"INSERT INTO transcript_directory VALUES(?1,?2,0,?3)",
params![session, sequence, role],
)
.unwrap();
}
let plan: String = db
.prepare(&format!("EXPLAIN QUERY PLAN {QUERY}"))
.unwrap()
.query_map(params!["a", 3, 0], |row| row.get::<_, String>(3))
.unwrap()
.map(Result::unwrap)
.collect::<Vec<_>>()
.join(" ");
assert!(
plan.contains("SEARCH transcript_directory") && plan.contains("INDEX directory_user"),
"{plan}"
);
for (anchor, expected) in [(3, 1), (4, 4)] {
let actual: (i64, i64) = db
.query_row(QUERY, params!["a", anchor, 0], |row| {
Ok((row.get(0)?, row.get(1)?))
})
.unwrap();
assert_eq!(actual, (expected, 0));
}
db.execute(
"INSERT INTO transcript_directory VALUES('a',4,300,?1)",
params!["user"],
)
.unwrap();
for (index, expected_index) in [(299, 0), (300, 300)] {
let actual: (i64, i64) = db
.query_row(QUERY, params!["a", 4, index], |row| {
Ok((row.get(0)?, row.get(1)?))
})
.unwrap();
assert_eq!(actual, (4, expected_index));
}
}
}