blob: 6f96b8eeff40784966ac6a86e6fab6386fc66a4b [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::StoreError;
use futures_util::TryStreamExt;
use rusqlite::{Connection, functions::FunctionFlags};
use sqlx::{Row, SqliteConnection};
/// Incremental normalization preserves whitespace across delta boundaries while
/// retaining at most 96 Unicode characters, including a truncation ellipsis.
#[derive(Default)]
struct Preview {
text: String,
count: usize,
space: bool,
full: bool,
}
impl Preview {
fn push(&mut self, text: &str) {
if self.full {
return;
}
for ch in text.chars() {
if matches!(ch, '\t'..='\r' | ' ' | '\u{a0}' | '\u{1680}' | '\u{2000}'..='\u{200a}' | '\u{2028}' | '\u{2029}' | '\u{202f}' | '\u{205f}' | '\u{3000}' | '\u{feff}')
{
self.space = !self.text.is_empty();
continue;
}
for next in self
.space
.then_some(' ')
.into_iter()
.chain(std::iter::once(ch))
{
if self.count == 96 {
self.text.pop();
self.text.push('…');
self.full = true;
return;
}
self.text.push(next);
self.count += 1;
}
self.space = false;
}
}
fn finish(self) -> Option<String> {
(!self.text.is_empty()).then_some(self.text)
}
}
pub(super) fn register_function(connection: &Connection) -> Result<(), StoreError> {
connection.create_scalar_function(
"catalog_preview",
1,
FunctionFlags::SQLITE_UTF8 | FunctionFlags::SQLITE_DETERMINISTIC,
|ctx| {
let mut preview = Preview::default();
if let Some(text) = ctx.get_raw(0).as_str_or_null()? {
preview.push(text);
}
Ok(preview.finish())
},
)?;
Ok(())
}
/// Only the sealed current step contributes partial messages. Nothing is added
/// to model_completed, so accepted model history retains its existing authority.
pub(super) async fn project(
connection: &mut SqliteConnection,
boundary: i64,
session: &str,
invocation: &str,
interrupted_step: Option<&str>,
) -> Result<(), StoreError> {
let step = if let Some(step) = interrupted_step {
Some(step.to_owned())
} else {
// Failed/cancelled terminal fallback: exclude steps already sealed before
// this boundary, including during a rebuild with later events present.
sqlx::query_scalar::<_, String>(
"WITH request AS (SELECT operation_id FROM event_log INDEXED BY catalog_model_boundaries
WHERE invocation_id = ?1 AND sequence < ?2
AND kind IN ('model_requested', 'model_completed', 'model_interrupted')
AND kind = 'model_requested' ORDER BY sequence DESC LIMIT 1)
SELECT operation_id FROM request WHERE NOT EXISTS (SELECT 1 FROM event_log AS result INDEXED BY operation_fact
WHERE result.operation_id = request.operation_id
AND result.operation_id IS NOT NULL
AND result.invocation_id = ?1 AND result.sequence < ?2
AND result.kind IN ('model_completed', 'model_interrupted'))
AND EXISTS (SELECT 1 FROM runtime_events WHERE sequence = ?2
AND json_extract(event_json, '$.fact.outcome.kind') IN ('failed', 'cancelled'))
LIMIT 1",
).bind(invocation).bind(boundary).fetch_optional(&mut *connection).await?
};
let Some(step) = step else {
return Ok(());
};
let visible: bool = sqlx::query_scalar(
"SELECT EXISTS (SELECT 1 FROM runtime_events request
JOIN runtime_events opening ON opening.invocation_id = request.invocation_id
AND opening.kind = 'invocation_opened'
WHERE request.kind = 'model_requested' AND request.invocation_id = ?1
AND request.operation_id = ?2
AND json_extract(opening.event_json, '$.fact.input.kind') IN ('message', 'continuation', 'handoff')
AND json_extract(request.event_json, '$.fact.purpose') = 'main')",
)
.bind(invocation)
.bind(&step)
.fetch_one(&mut *connection)
.await?;
if !visible {
return Ok(());
}
let starts = sqlx::query(
"SELECT sequence, catalog_time(json_extract(event_json, '$.recorded_at')),
json_extract(event_json, '$.fact.event.data.id'),
json_extract(event_json, '$.fact.event.data.text_kind') = 'text', event_id
FROM event_log INDEXED BY catalog_part_starts
WHERE json_extract(event_json, '$.invocation.session_id') = ?1
AND invocation_id = ?2 AND json_extract(event_json, '$.fact.step_id') = ?3
AND kind = 'model_observed'
AND json_extract(event_json, '$.fact.event.kind') = 'part_started'
AND sequence < ?4 ORDER BY sequence",
)
.bind(session)
.bind(invocation)
.bind(&step)
.bind(boundary)
.fetch_all(&mut *connection)
.await?;
for (ordinal, row) in starts.into_iter().enumerate() {
let start: i64 = row.try_get(0)?;
let timestamp: i64 = row.try_get(1)?;
let part: String = row.try_get(2)?;
let is_text: bool = row.try_get(3)?;
let message_id: String = row.try_get(4)?;
let mut preview = Preview::default();
if is_text {
let mut rows = sqlx::query(
"SELECT json_extract(event_json, '$.fact.event.data.text')
FROM event_log INDEXED BY catalog_partial_deltas
WHERE invocation_id = ?1 AND json_extract(event_json, '$.fact.step_id') = ?2
AND json_extract(event_json, '$.fact.event.data.id') = ?3
AND kind = 'model_observed'
AND json_extract(event_json, '$.fact.event.kind') = 'part_delta'
AND sequence > ?4 AND sequence < ?5 ORDER BY sequence",
)
.bind(invocation)
.bind(&step)
.bind(part)
.bind(start)
.bind(boundary)
.fetch(&mut *connection);
while !preview.full {
let Some(row) = rows.try_next().await? else {
break;
};
// Borrow each SQLite value; never concatenate or deserialize the
// complete stream. Once the prefix is full, stop reading deltas.
preview.push(row.try_get::<&str, _>(0)?);
}
}
sqlx::query("INSERT OR IGNORE INTO catalog_messages VALUES (?, ?, ?, ?, ?, ?)")
.bind(boundary)
.bind(ordinal as i64 + 1)
.bind(session)
.bind(timestamp)
.bind(preview.finish())
.bind(message_id)
.execute(&mut *connection)
.await?;
}
Ok(())
}