blob: 5485c63814cef55b28d6a3e3e4300e5ecc804f2d [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, sequence_number};
use maka_agent_facts::event::StoredEvent;
use maka_presentation::ProjectionError;
use rusqlite::{Connection as RawConnection, OptionalExtension, params};
use sqlx::SqliteConnection;
use sqlx::sqlite::SqliteWorkerConnection;
const HIGH: &str = r#"SELECT COALESCE(MAX(sequence), 0) FROM event_log"#;
const EXEC_BOUNDARY: &str = r#"SELECT sequence FROM runtime_events
WHERE invocation_id = ?1 AND sequence <= ?2 AND kind = 'invocation_opened'
UNION
SELECT sequence FROM runtime_events
WHERE invocation_id = ?1 AND sequence <= ?2 AND kind = 'executor_started'
UNION
SELECT sequence FROM runtime_events
WHERE invocation_id = ?1 AND sequence <= ?2 AND ?3 AND sequence = ?2
UNION
SELECT sequence FROM runtime_events
WHERE invocation_id = ?1 AND sequence <= ?2
AND kind IN ('executor_observed','executor_completed')
AND json_extract(event_json, '$.fact.output.toolCallId') =
(SELECT json_extract(event_json, '$.fact.output.toolCallId') FROM runtime_events WHERE sequence = ?2)
ORDER BY sequence"#;
const EXEC_REST: &str = r#"SELECT sequence FROM runtime_events
WHERE invocation_id = ?1 AND sequence <= ?2 AND (
kind IN ('invocation_opened','executor_started')
OR (?3 AND sequence = ?2)
OR (kind IN ('executor_observed','executor_completed') AND (
NOT ?3 OR NOT EXISTS(SELECT 1 FROM runtime_events boundary WHERE boundary.sequence = ?2 AND boundary.kind = 'executor_observed')
OR json_extract(event_json, '$.fact.output.toolCallId') =
(SELECT json_extract(event_json, '$.fact.output.toolCallId') FROM runtime_events WHERE sequence = ?2))))
ORDER BY sequence"#;
const STEP: &str = r#"SELECT sequence FROM runtime_events
WHERE invocation_id = ?1 AND sequence <= ?3 AND kind = 'invocation_opened'
UNION
SELECT sequence FROM runtime_events
WHERE invocation_id = ?1 AND sequence <= ?3
AND sequence IN (SELECT value FROM json_each(?4))
UNION
SELECT sequence FROM runtime_events
WHERE invocation_id = ?1 AND sequence <= ?3
AND ?2 IS NOT NULL AND kind = 'model_requested' AND operation_id = ?2
UNION
SELECT sequence FROM runtime_events
WHERE invocation_id = ?1 AND sequence <= ?3
AND ?5 AND ?2 IS NOT NULL AND kind = 'model_completed' AND operation_id = ?2
UNION
SELECT sequence FROM runtime_events
WHERE invocation_id = ?1 AND sequence <= ?3 AND ?2 IS NOT NULL
AND kind = 'model_observed'
AND CAST(json_extract(event_json, '$.fact.step_id') AS TEXT) = ?2
AND json_extract(event_json, '$.fact.step_id') = ?2
ORDER BY sequence"#;
const FACT: &str = r#"SELECT event_json FROM runtime_events WHERE sequence = ?"#;
const PENDING: &str = r#"SELECT operation_id FROM runtime_events AS request
WHERE invocation_id = ?1 AND sequence <= ?2 AND kind = 'model_requested'
AND EXISTS(SELECT 1 FROM runtime_events opening
WHERE opening.invocation_id = request.invocation_id AND opening.kind = 'invocation_opened'
AND json_extract(opening.event_json, '$.fact.input.kind') IN ('message', 'continuation', 'handoff'))
AND json_extract(request.event_json, '$.fact.purpose') = 'main'
AND NOT EXISTS(SELECT 1 FROM runtime_events AS boundary
WHERE boundary.invocation_id = request.invocation_id
AND boundary.operation_id = request.operation_id
AND boundary.kind IN ('model_completed', 'model_interrupted')
AND boundary.sequence <= ?2)
ORDER BY sequence DESC LIMIT 1"#;
// Tool boundaries keep their original ancestry path and do not inspect
// external execution or unresolved model requests.
const PLAN: &str = r#"WITH boundary(kind) AS (
SELECT CASE WHEN ?3 THEN (
SELECT kind FROM runtime_events WHERE sequence=?2 AND invocation_id=?1
) END
), classified AS (
SELECT kind, COALESCE(kind IN ('tool_dispatched','tool_rejected','tool_settled'),0) AS tool
FROM boundary
)
SELECT kind, tool, CASE WHEN tool THEN 0 ELSE EXISTS(
SELECT 1 FROM runtime_events WHERE invocation_id=?1 AND sequence<=?2 AND kind='executor_started'
) END AS external FROM classified"#;
#[derive(sqlx::FromRow)]
struct Metadata {
kind: Option<String>,
tool: bool,
external: bool,
}
/// Live and preparation readers use the same metadata and selection policy.
/// Both queries remain inside the reader's original transaction.
pub(super) async fn selected(
tx: &mut SqliteConnection,
invocation: &str,
step: Option<&str>,
through: i64,
boundary: bool,
) -> Result<Vec<i64>, StoreError> {
let invocation = invocation.to_owned();
let step = step.map(str::to_owned);
crate::connection::worker_query(tx, move |worker| {
Box::pin(async move {
selected_on_worker(worker, &invocation, step.as_deref(), through, boundary).await
})
})
.await
}
pub(super) async fn selected_on_worker(
tx: &mut SqliteWorkerConnection<'_>,
invocation: &str,
step: Option<&str>,
through: i64,
boundary: bool,
) -> Result<Vec<i64>, StoreError> {
let metadata = sqlx::query_as::<_, Metadata>(PLAN)
.bind(invocation)
.bind(through)
.bind(boundary)
.fetch_one(&mut *tx)
.await?;
let mut selection =
if let Some(selection) = plan(metadata, invocation, step, through, boundary)? {
selection
} else {
// SAFETY: the worker exclusively owns this borrowed handle. All
// temporary raw statements are dropped before SQLx resumes.
let db = unsafe { RawConnection::from_handle(tx.as_raw_handle().as_ptr()) }?;
db.set_prepared_statement_cache_capacity(100);
super::tools::selection(&db, invocation, through)?
};
if let Some(slot) = selection.pending_step_slot() {
*slot = match sqlx::query_scalar::<_, Option<String>>(PENDING)
.bind(invocation)
.bind(through)
.fetch_optional(&mut *tx)
.await?
{
Some(Some(step)) => Some(step),
None => None,
Some(None) => {
return Err(rusqlite::Error::InvalidColumnType(
0,
"operation_id".into(),
rusqlite::types::Type::Null,
)
.into());
}
};
}
selection.read(tx).await
}
pub(super) async fn fence(tx: &mut SqliteConnection, through: u64) -> Result<i64, StoreError> {
let through = i64::try_from(through).map_err(|_| ProjectionError::OutOfRange)?;
let high: i64 = sqlx::query_scalar(HIGH).fetch_one(tx).await?;
checked_fence(through, high)
}
fn checked_fence(through: i64, high: i64) -> Result<i64, StoreError> {
if through > high {
return Err(StoreError::InvalidTransition(
"transcript fence exceeds committed log".into(),
));
}
Ok(through)
}
pub(super) async fn fact(
tx: &mut SqliteConnection,
sequence: i64,
) -> Result<StoredEvent, StoreError> {
let json: String = sqlx::query_scalar(FACT)
.bind(sequence)
.fetch_one(tx)
.await?;
decoded_fact(sequence, &json)
}
pub(super) fn fact_on_handle(db: &RawConnection, sequence: i64) -> Result<StoredEvent, StoreError> {
let json: String = db
.prepare_cached(FACT)?
.query_row([sequence], |row| row.get(0))?;
decoded_fact(sequence, &json)
}
fn decoded_fact(sequence: i64, json: &str) -> Result<StoredEvent, StoreError> {
Ok(StoredEvent {
sequence: sequence_number(sequence)?,
event: serde_json::from_str(json)?,
})
}
pub(super) async fn pending_step(
tx: &mut SqliteConnection,
invocation: &str,
through: i64,
) -> Result<Option<String>, StoreError> {
let invocation = invocation.to_owned();
crate::connection::worker_query(tx, move |worker| {
Box::pin(async move { pending_step_on_worker(worker, &invocation, through).await })
})
.await
}
pub(super) async fn pending_step_on_worker(
tx: &mut SqliteWorkerConnection<'_>,
invocation: &str,
through: i64,
) -> Result<Option<String>, StoreError> {
Ok(sqlx::query_scalar(PENDING)
.bind(invocation)
.bind(through)
.fetch_optional(tx)
.await?)
}
fn pending_step_on_handle(
db: &RawConnection,
invocation: &str,
through: i64,
) -> Result<Option<String>, StoreError> {
Ok(db
.prepare_cached(PENDING)?
.query_row(params![invocation, through], |row| row.get(0))
.optional()?)
}
fn ids(
db: &RawConnection,
sql: &str,
parameters: impl rusqlite::Params,
) -> Result<Vec<i64>, StoreError> {
Ok(db
.prepare_cached(sql)?
.query_map(parameters, |row| row.get(0))?
.collect::<Result<Vec<_>, _>>()?)
}
/// Select the actual opening, one step and its exact boundary evidence.
/// This is the only selection policy for both preparation and live readers.
pub(super) fn selected_on_handle(
db: &RawConnection,
invocation: &str,
step: Option<&str>,
through: i64,
boundary: bool,
) -> Result<Vec<i64>, StoreError> {
let metadata =
db.prepare_cached(PLAN)?
.query_row(params![invocation, through, boundary], |row| {
Ok(Metadata {
kind: row.get(0)?,
tool: row.get(1)?,
external: row.get(2)?,
})
})?;
let mut selection = match plan(metadata, invocation, step, through, boundary)? {
Some(selection) => selection,
None => super::tools::selection(db, invocation, through)?,
};
if let Some(slot) = selection.pending_step_slot() {
*slot = pending_step_on_handle(db, invocation, through)?;
}
selection.read_on_handle(db)
}
/// None requests the existing tool ancestry planner; all other branches own
/// their complete query inputs without retaining a connection or evidence.
fn plan(
metadata: Metadata,
invocation: &str,
step: Option<&str>,
through: i64,
boundary: bool,
) -> Result<Option<Selection>, StoreError> {
if boundary && metadata.kind.is_none() {
return Err(rusqlite::Error::QueryReturnedNoRows.into());
}
if metadata.tool {
return Ok(None);
}
if metadata.external {
return Ok(Some(
if metadata.kind.as_deref() == Some("executor_observed") {
Selection::ExecutorBoundary {
invocation: invocation.to_owned(),
through,
boundary,
}
} else {
Selection::ExecutorRest {
invocation: invocation.to_owned(),
through,
boundary,
}
},
));
}
let boundaries = serde_json::to_string(if boundary {
std::slice::from_ref(&through)
} else {
&[]
})?;
Ok(Some(Selection::Step {
invocation: invocation.to_owned(),
step: step.map(str::to_owned),
through,
boundaries,
completed: false,
}))
}
/// Owned query inputs selected under the current transaction. No evidence
/// results or borrowed connection state survive the worker callback.
pub(super) enum Selection {
Step {
invocation: String,
step: Option<String>,
through: i64,
boundaries: String,
completed: bool,
},
ExecutorBoundary {
invocation: String,
through: i64,
boundary: bool,
},
ExecutorRest {
invocation: String,
through: i64,
boundary: bool,
},
}
impl Selection {
// A standalone tool deliberately has no model step. Only ordinary step
// selection may resolve a pending request, retaining NULL/error semantics.
fn pending_step_slot(&mut self) -> Option<&mut Option<String>> {
match self {
Self::Step {
step,
completed: false,
..
} if step.is_none() => Some(step),
_ => None,
}
}
fn sql(&self) -> &'static str {
match self {
Self::Step { .. } => STEP,
Self::ExecutorBoundary { .. } => EXEC_BOUNDARY,
Self::ExecutorRest { .. } => EXEC_REST,
}
}
fn read_on_handle(&self, db: &RawConnection) -> Result<Vec<i64>, StoreError> {
match self {
Self::Step {
invocation,
step,
through,
boundaries,
completed,
} => ids(
db,
self.sql(),
params![invocation, step, through, boundaries, completed],
),
Self::ExecutorBoundary {
invocation,
through,
boundary,
}
| Self::ExecutorRest {
invocation,
through,
boundary,
} => ids(db, self.sql(), params![invocation, through, boundary]),
}
}
async fn read(&self, tx: &mut SqliteWorkerConnection<'_>) -> Result<Vec<i64>, StoreError> {
// UNION removes overlaps; CAST uses the index while original JSON
// equality preserves malformed non-string step-ID selection semantics.
let query = sqlx::query_scalar(self.sql());
Ok(match self {
Self::Step {
invocation,
step,
through,
boundaries,
completed,
} => {
query
.bind(invocation)
.bind(step)
.bind(through)
.bind(boundaries)
.bind(completed)
.fetch_all(tx)
.await?
}
Self::ExecutorBoundary {
invocation,
through,
boundary,
}
| Self::ExecutorRest {
invocation,
through,
boundary,
} => {
query
.bind(invocation)
.bind(through)
.bind(boundary)
.fetch_all(tx)
.await?
}
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::EventLog;
use serde_json::json;
use sqlx::Connection;
#[tokio::test]
async fn cached_selection_preserves_json_step_types_fences_and_null_rebinding() {
let temp = tempfile::tempdir().unwrap();
let log = EventLog::open(&temp.path().join("evidence.sqlite"))
.await
.unwrap();
log.connection.run(|db| Box::pin(async move {
// Deliberately seed one malformed numeric step ID in the real schema.
// Selection must exclude it before canonical fact decoding can occur.
for (sequence, kind, operation, fact) in [
(1, "invocation_opened", None, json!({"input":{"kind":"message"}})),
(2, "model_requested", Some("42"), json!({"purpose":"main","step_id":"42"})),
(3, "model_observed", None, json!({"step_id":"42"})),
(4, "model_observed", None, json!({"step_id":42})),
(5, "model_completed", Some("42"), json!({"step_id":"42"})),
(6, "model_observed", None, json!({"step_id":"42"})),
] {
let event = json!({"invocation":{"session_id":"session"},"fact":fact});
sqlx::query("INSERT INTO event_log(sequence,event_id,invocation_id,kind,operation_id,event_json) VALUES(?,?,'invocation',?,?,?)")
.bind(sequence).bind(format!("event-{sequence}")).bind(kind)
.bind(operation).bind(event.to_string()).execute(&mut *db).await?;
}
let mut tx = db.begin().await?;
for (step, through, boundary, expected) in [
(Some("42"), 5, false, vec![1, 2, 3]),
(None, 5, false, vec![1]),
(None, 3, false, vec![1, 2, 3]),
(None, 3, true, vec![1, 2, 3]),
(Some("42"), 7, false, vec![1, 2, 3, 6]),
(None, 7, false, vec![1]),
(Some("42"), 5, true, vec![1, 2, 3, 5]),
(Some("42"), 3, false, vec![1, 2, 3]),
(Some("missing"), 5, false, vec![1]),
(Some("42"), 5, false, vec![1, 2, 3]),
] {
assert_eq!(selected(&mut tx, "invocation", step, through, boundary).await?, expected);
let synchronous = tx.run_on_worker(move |handle| {
// SAFETY: no borrowed handle or statement escapes this callback.
let raw = unsafe { RawConnection::from_handle(handle.as_ptr()) }?;
selected_on_handle(&raw, "invocation", step, through, boundary)
}).await??;
assert_eq!(synchronous, expected);
}
assert!(selected(&mut tx, "invocation", None, 7, true).await.is_err());
let missing_boundary = tx.run_on_worker(move |handle| {
// SAFETY: the raw handle and all statements stay within this callback.
let raw = unsafe { RawConnection::from_handle(handle.as_ptr()) }?;
selected_on_handle(&raw, "invocation", None, 7, true)
}).await?;
assert!(missing_boundary.is_err());
// A malformed pending request with NULL identity must remain an
// error, rather than becoming indistinguishable from no request.
sqlx::query("INSERT INTO event_log(sequence,event_id,invocation_id,kind,event_json) VALUES(8,'event-8','invocation','model_requested',?)")
.bind(json!({"invocation":{"session_id":"session"},"fact":{"purpose":"main"}}).to_string())
.execute(&mut *tx).await?;
assert!(selected(&mut tx, "invocation", None, 8, false).await.is_err());
let malformed_pending = tx.run_on_worker(move |handle| {
// SAFETY: the handle and statements stay within this callback.
let raw = unsafe { RawConnection::from_handle(handle.as_ptr()) }?;
selected_on_handle(&raw, "invocation", None, 8, false)
}).await?;
assert!(malformed_pending.is_err());
assert_eq!(selected(&mut tx, "invocation", Some("42"), 8, false).await?, vec![1, 2, 3, 6]);
tx.rollback().await?;
Ok(())
})).await.unwrap();
log.close().await.unwrap();
}
}