blob: c9b81d33052a69a7790eda01fe9e14e020377b3f [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, sessions};
use maka_agent_facts::event::Fact;
use maka_agent_facts::event::Invocation;
use maka_agent_facts::event::InvocationOutcome;
use maka_agent_facts::event::RuntimeEvent;
use maka_agent_facts::input::InvocationInput;
use sqlx::Connection;
mod root;
/// A bounded projection of canonical boundaries, never a model-history prefix.
pub struct TurnBoundary {
pub opening_event_id: String,
pub invocation: Invocation,
pub input: InvocationInput,
pub state: InvocationState,
root: Option<root::Origin>,
}
impl TurnBoundary {
/// Stable client identity. Execution and cleanup still use `invocation`.
pub fn root_invocation(&self) -> &Invocation {
self.root
.as_ref()
.map_or(&self.invocation, |root| &root.invocation)
}
pub fn root_input(&self) -> &InvocationInput {
self.root.as_ref().map_or(&self.input, |root| &root.input)
}
pub fn root_opening_event_id(&self) -> &str {
self.root
.as_ref()
.map_or(&self.opening_event_id, |root| &root.event_id)
}
}
pub enum InvocationState {
Admitted,
Running,
WaitingForUser,
Ended {
event_id: String,
outcome: InvocationOutcome,
},
}
impl InvocationState {
/// A sealed physical Run still owns unfinished logical work.
pub fn terminal_outcome(&self) -> Option<&InvocationOutcome> {
match self {
Self::Ended {
outcome: InvocationOutcome::HandoffPaused { .. },
..
}
| Self::Admitted
| Self::Running
| Self::WaitingForUser => None,
Self::Ended { outcome, .. } => Some(outcome),
}
}
}
impl EventLog {
/// Compact canonical receipt keyed by the globally unique opening identity.
/// No plugin ledger or current Session tip is used to reconstruct acceptance.
pub async fn continuation_receipt(
&self,
invocation_id: &str,
) -> Result<
Option<(
Invocation,
maka_agent_facts::continuation::RunBoundary,
String,
)>,
StoreError,
> {
self.validate_root()?;
sessions::validate_id(invocation_id)?;
let invocation_id = invocation_id.to_owned();
self.connection
.run(move |connection| {
Box::pin(async move {
let row: Option<(String, Option<String>, Option<String>)> = sqlx::query_as(
"SELECT json_extract(event_json, '$.invocation'),
json_extract(event_json, '$.fact.input.claim.source'),
json_extract(event_json, '$.fact.input.request_fingerprint')
FROM runtime_events WHERE invocation_id = ? AND kind = 'invocation_opened'",
)
.bind(invocation_id)
.fetch_optional(connection)
.await?;
row.map(|(invocation, source, fingerprint)| {
Ok((
serde_json::from_str(&invocation)?,
serde_json::from_str(&source.ok_or(StoreError::EventConflict)?)?,
fingerprint.ok_or(StoreError::EventConflict)?,
))
})
.transpose()
})
})
.await
}
/// Exact physical invocation and Run-local event count in one snapshot.
/// Never substitutes the Session tip or a root-global stream position.
pub async fn invocation_boundary(
&self,
session_id: &str,
invocation_id: &str,
) -> Result<Option<(TurnBoundary, u64)>, StoreError> {
self.validate_root()?;
sessions::validate_id(session_id)?;
sessions::validate_id(invocation_id)?;
let session = session_id.to_owned();
let invocation = invocation_id.to_owned();
self.connection
.run(move |connection| {
Box::pin(async move {
let mut tx = connection.begin().await?;
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?;
let result = match opening {
Some(opening) => {
let boundary =
project(&mut tx, serde_json::from_str(&opening)?).await?;
let high_water: i64 = sqlx::query_scalar(
"SELECT COUNT(*) FROM runtime_events WHERE invocation_id = ?",
)
.bind(&boundary.invocation.invocation_id)
.fetch_one(&mut *tx)
.await?;
Some((boundary, high_water as u64))
}
None => None,
};
tx.commit().await?;
Ok(result)
})
})
.await
}
pub async fn latest_turn_boundary(
&self,
session_id: &str,
) -> Result<Option<TurnBoundary>, StoreError> {
self.validate_root()?;
sessions::validate_id(session_id)?;
let session = session_id.to_owned();
self.connection
.run(move |connection| {
Box::pin(async move {
let mut tx = connection.begin().await?;
read(&mut tx, &session, None).await
})
})
.await
}
/// Read the frozen execution boundary without loading message or model bodies.
pub async fn invocation_configuration(
&self,
invocation: &Invocation,
) -> Result<Option<maka_agent_facts::execution::InvocationConfiguration>, StoreError> {
self.validate_root()?;
let invocation = invocation.clone();
self.connection.run(move |connection| Box::pin(async move {
let configuration: Option<String> = sqlx::query_scalar(
"SELECT json_extract(event_json, '$.fact.configuration') FROM runtime_events
WHERE invocation_id = ?1 AND kind = 'invocation_opened'
AND json_extract(event_json, '$.invocation.session_id') = ?2
AND json_extract(event_json, '$.invocation.run_id') = ?3
AND json_extract(event_json, '$.invocation.turn_id') = ?4"
).bind(&invocation.invocation_id).bind(&invocation.session_id).bind(&invocation.run_id).bind(&invocation.turn_id)
.fetch_optional(connection).await?.flatten();
configuration.map(|value| serde_json::from_str(&value).map_err(StoreError::from)).transpose()
})).await
}
/// Exact physical Run, including an earlier Run of the same logical Turn.
pub async fn run_boundary(
&self,
session_id: &str,
run_id: &str,
) -> Result<Option<TurnBoundary>, StoreError> {
self.validate_root()?;
sessions::validate_id(session_id)?;
sessions::validate_id(run_id)?;
let (session_id, run_id) = (session_id.to_owned(), run_id.to_owned());
self.connection
.run(move |connection| {
Box::pin(async move {
let mut tx = connection.begin().await?;
let Some(invocation) = run_invocation(&mut tx, &session_id, &run_id).await? else {
return Ok(None);
};
let opening: String = sqlx::query_scalar(
"SELECT event_json FROM runtime_events WHERE invocation_id = ? AND kind = 'invocation_opened'",
).bind(invocation).fetch_one(&mut *tx).await?;
let result = project(&mut tx, serde_json::from_str(&opening)?).await?;
tx.commit().await?;
Ok(Some(result))
})
})
.await
}
/// Only opening, logical root and terminal bodies are decoded, irrespective of streamed history.
/// Reading control/status must remain possible after context budgets fill.
pub async fn turn_boundary(
&self,
session_id: &str,
turn_id: &str,
) -> Result<Option<TurnBoundary>, StoreError> {
self.validate_root()?;
sessions::validate_id(session_id)?;
sessions::validate_id(turn_id)?;
let session_id = session_id.to_owned();
let turn_id = turn_id.to_owned();
self.connection
.run(move |connection| {
Box::pin(async move {
let mut transaction = connection.begin().await?;
let result = read(&mut transaction, &session_id, Some(&turn_id)).await?;
transaction.commit().await?;
Ok(result)
})
})
.await
}
}
/// Caller holds one read transaction when combining with metadata or a cursor.
pub(crate) async fn read(
connection: &mut sqlx::SqliteConnection,
session_id: &str,
turn_id: Option<&str>,
) -> Result<Option<TurnBoundary>, StoreError> {
let opening: Option<String> = sqlx::query_scalar(
"SELECT event_json FROM runtime_events
WHERE kind = 'invocation_opened'
AND json_extract(event_json, '$.invocation.session_id') = ?1
AND (?2 IS NULL OR json_extract(event_json, '$.invocation.turn_id') = ?2)
ORDER BY sequence DESC LIMIT 1",
)
.bind(session_id)
.bind(turn_id)
.fetch_optional(&mut *connection)
.await?;
let Some(opening) = opening else {
return Ok(None);
};
Ok(Some(
project(connection, serde_json::from_str(&opening)?).await?,
))
}
/// Fail closed on ambiguous legacy/synthetic identities; never choose a newer Run.
pub(crate) async fn run_invocation(
connection: &mut sqlx::SqliteConnection,
session: &str,
run: &str,
) -> Result<Option<String>, StoreError> {
let ids: Vec<String> = sqlx::query_scalar(
"SELECT invocation_id FROM runtime_events WHERE kind = 'invocation_opened'
AND json_extract(event_json, '$.invocation.session_id') = ?
AND json_extract(event_json, '$.invocation.run_id') = ? LIMIT 2",
)
.bind(session)
.bind(run)
.fetch_all(connection)
.await?;
if ids.len() > 1 {
return Err(StoreError::InvalidTransition(
"Run names multiple invocations".into(),
));
}
Ok(ids.into_iter().next())
}
pub(crate) async fn project(
connection: &mut sqlx::SqliteConnection,
opening: RuntimeEvent,
) -> Result<TurnBoundary, StoreError> {
let Fact::InvocationOpened { input, .. } = opening.fact else {
return Err(StoreError::InvalidTransition(
"invalid stored invocation opening".into(),
));
};
let root = root::read(connection, &opening.invocation, &input).await?;
let terminal: Option<String> = sqlx::query_scalar(
"SELECT event_json FROM runtime_events WHERE invocation_id = ? AND kind = 'invocation_ended'",
).bind(&opening.invocation.invocation_id).fetch_optional(&mut *connection).await?;
let state = if let Some(terminal) = terminal {
let terminal: RuntimeEvent = serde_json::from_str(&terminal)?;
let Fact::InvocationEnded { outcome } = terminal.fact else {
return Err(StoreError::InvalidTransition(
"invalid stored invocation terminal".into(),
));
};
if terminal.invocation != opening.invocation {
return Err(StoreError::InvalidTransition(
"stored invocation identity changed".into(),
));
}
InvocationState::Ended {
event_id: terminal.id,
outcome,
}
} else if crate::interactions::pending(connection, &opening.invocation.session_id)
.await?
.iter()
.any(|request| {
request.run_id == opening.invocation.run_id
&& request.turn_id == opening.invocation.turn_id
})
{
InvocationState::WaitingForUser
} else {
let requested: bool = sqlx::query_scalar(
"SELECT EXISTS(SELECT 1 FROM runtime_events WHERE invocation_id = ? AND kind = 'model_requested')",
).bind(&opening.invocation.invocation_id).fetch_one(&mut *connection).await?;
if requested {
InvocationState::Running
} else {
InvocationState::Admitted
}
};
Ok(TurnBoundary {
root,
opening_event_id: opening.id,
invocation: opening.invocation,
input,
state,
})
}