blob: ac17dce9a6f139a4d5ed4333e667e006b2eac07f [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.
*/
//! Durable pending messages. Consuming one is part of the canonical event transaction.
use crate::{EventLog, StoreError};
use maka_agent_facts::event::Fact;
use maka_agent_facts::event::Invocation;
use maka_agent_facts::event::RuntimeEvent;
use maka_agent_facts::message::MessageDisposition;
use maka_agent_facts::message::RootSourceMessage;
use serde::{Deserialize, Serialize};
use sqlx::{Connection, SqliteConnection};
pub(crate) mod insert;
mod receipts;
pub use receipts::MessageSubmitReceipt;
/// Dispatcher-owned, non-secret prerequisites for accepted work. The store
/// preserves this value; only the named dispatcher interprets its typed basis.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PendingExecution {
pub owner: String,
pub basis: serde_json::Value,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PendingMessageAdmission {
pub execution: PendingExecution,
pub invocation: Invocation,
/// Promotion may target a later Run without rewriting original admission ownership.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub steering_invocation: Option<Invocation>,
pub source: RootSourceMessage,
/// Frozen prerequisites for delivery or promotion; never re-read Skills
/// while handing an accepted message to another Run.
#[serde(default, skip_serializing_if = "std::collections::BTreeSet::is_empty")]
pub required_tools: std::collections::BTreeSet<String>,
pub admitted_at: u64,
}
impl PendingMessageAdmission {
pub fn steering_target(&self) -> &Invocation {
self.steering_invocation
.as_ref()
.unwrap_or(&self.invocation)
}
fn validate(&self) -> Result<(), StoreError> {
crate::sessions::validate_id(&self.execution.owner)?;
for id in [
&self.invocation.session_id,
&self.invocation.turn_id,
&self.invocation.run_id,
&self.invocation.invocation_id,
] {
crate::sessions::validate_id(id)?;
}
self.source.validate().map_err(invalid)?;
if let Some(target) = &self.steering_invocation {
if self.source.disposition != MessageDisposition::Steering
|| target.session_id != self.invocation.session_id
{
return Err(invalid("invalid promoted steering target"));
}
for id in [&target.turn_id, &target.run_id, &target.invocation_id] {
crate::sessions::validate_id(id)?;
}
}
if let Some(intent) = &self.source.submitted_intent {
maka_agent_facts::input::validate_selection_session(
&intent.input_selection_sources,
&self.invocation.session_id,
)
.map_err(invalid)?;
}
if self
.source
.submitted_intent
.as_ref()
.is_some_and(maka_agent_facts::message::SubmittedTurnIntent::is_exact_turn)
&& self.source.disposition != MessageDisposition::TurnStarted
{
return Err(invalid(
"exact Turn intent cannot enter a running Turn queue",
));
}
if self.admitted_at > maka_agent_facts::configuration::validation::MAX_SAFE_INTEGER {
return Err(invalid("invalid message admission time"));
}
Ok(())
}
}
impl EventLog {
/// Startup keyset scan; never materialize every Session's pending content.
pub async fn pending_message_sessions(
&self,
owner: &str,
after: Option<&str>,
) -> Result<Vec<String>, StoreError> {
self.validate_root()?;
if let Some(after) = after {
crate::sessions::validate_id(after)?;
}
crate::sessions::validate_id(owner)?;
let owner = owner.to_owned();
let after = after.map(str::to_owned);
self.connection
.run(move |connection| {
Box::pin(async move {
Ok(sqlx::query_scalar(
"SELECT DISTINCT session_id FROM message_admissions
WHERE (?1 IS NULL OR session_id > ?1)
AND json_extract(record_json, '$.execution.owner') = ?2
ORDER BY session_id LIMIT 64",
)
.bind(after)
.bind(owner)
.fetch_all(connection)
.await?)
})
})
.await
}
/// Caller owns Session admission; the independent SQL owner survives waiter cancellation.
pub async fn admit_message(
&self,
admission: PendingMessageAdmission,
) -> Result<PendingMessageAdmission, StoreError> {
self.validate_root()?;
self.connection
.write(WriteAdmission(admission), self.commits.clone())
.await
}
pub async fn message_admission(
&self,
session: &str,
message: &str,
) -> Result<Option<PendingMessageAdmission>, StoreError> {
self.validate_root()?;
crate::sessions::validate_id(session)?;
crate::sessions::validate_id(message)?;
let session = session.to_owned();
let message = message.to_owned();
self.connection
.run(move |connection| {
Box::pin(async move { read(connection, &session, &message).await })
})
.await
}
pub async fn pending_messages(
&self,
session: &str,
) -> Result<Vec<PendingMessageAdmission>, StoreError> {
self.validate_root()?;
crate::sessions::validate_id(session)?;
let session = session.to_owned();
self.connection
.run(move |connection| {
Box::pin(async move {
let mut tx = connection.begin().await?;
pending(&mut tx, &session).await
})
})
.await
}
}
pub(super) async fn pending(
tx: &mut SqliteConnection,
session: &str,
) -> Result<Vec<PendingMessageAdmission>, StoreError> {
let rows: Vec<(String, String)> = sqlx::query_as(
"SELECT message_id, record_json FROM message_admissions WHERE session_id = ? ORDER BY position, message_id"
).bind(session).fetch_all(tx).await?;
rows.into_iter()
.map(|(id, json)| decode(&json, session, &id))
.collect()
}
pub(crate) async fn read(
connection: &mut SqliteConnection,
session: &str,
message: &str,
) -> Result<Option<PendingMessageAdmission>, StoreError> {
let json: Option<String> = sqlx::query_scalar(
"SELECT record_json
FROM message_admissions WHERE session_id = ? AND message_id = ?",
)
.bind(session)
.bind(message)
.fetch_optional(connection)
.await?;
json.map(|json| decode(&json, session, message)).transpose()
}
fn decode(json: &str, session: &str, message: &str) -> Result<PendingMessageAdmission, StoreError> {
let record: PendingMessageAdmission = serde_json::from_str(json)?;
record.validate()?;
if record.invocation.session_id != session || record.source.message.message_id != message {
return Err(invalid("message admission identity differs from its key"));
}
Ok(record)
}
/// No handoff flag: only the atomic appearance of the canonical delivery retires pending work.
pub(crate) async fn consume(
connection: &mut SqliteConnection,
event: &RuntimeEvent,
) -> Result<(), StoreError> {
for id in crate::message_sources::identities(event) {
if crate::message_queue::cancelled(connection, &event.invocation.session_id, id).await? {
return Err(invalid("cancelled message cannot be delivered"));
}
let Some(pending) = read(connection, &event.invocation.session_id, id).await? else {
continue;
};
let matches = match &event.fact {
Fact::MessageSteered { message, source } => {
*pending.steering_target() == event.invocation
&& pending.source.disposition == MessageDisposition::Steering
&& match source.as_deref() {
Some(source) => source == &pending.source,
None => {
pending.source.submitted_intent.is_none()
&& pending.source.message == **message
}
}
}
Fact::InvocationOpened { .. } => {
crate::message_sources::roots(event).iter().any(|source| {
source == &pending.source
&& (source.disposition != MessageDisposition::TurnStarted
|| pending.invocation == event.invocation)
})
}
_ => false,
};
if !matches {
return Err(invalid(
"canonical delivery differs from its pending admission",
));
}
sqlx::query("DELETE FROM message_admissions WHERE session_id = ? AND message_id = ?")
.bind(&event.invocation.session_id)
.bind(id)
.execute(&mut *connection)
.await?;
crate::message_queue::bump(connection, &event.invocation.session_id).await?;
}
Ok(())
}
fn invalid(message: &str) -> StoreError {
StoreError::InvalidTransition(message.into())
}
pub(crate) struct WriteAdmission(PendingMessageAdmission);
impl crate::writeback::Statement for WriteAdmission {
type Value = PendingMessageAdmission;
type Output = PendingMessageAdmission;
fn execute(
self,
connection: &mut SqliteConnection,
) -> crate::connection::BoxFuture<'_, Result<crate::writeback::Prepared<Self::Value>, StoreError>>
{
Box::pin(async move {
let changed = insert::insert(connection, &self.0, insert::Owner::Unsealed)
.await?
.is_some();
Ok(crate::writeback::Prepared {
value: self.0,
changed,
notification: if changed {
crate::writeback::Notification::Metadata
} else {
crate::writeback::Notification::None
},
})
})
}
fn committed(value: Self::Value) -> Self::Output {
value
}
fn isolated_rejection(error: &StoreError) -> bool {
matches!(
error,
StoreError::InvalidTransition(_)
| StoreError::SessionNotFound
| StoreError::SessionBusy
| StoreError::SessionRetired
| StoreError::PermissionRevoked
| StoreError::ImportedInvocation
| StoreError::MaterialCollected
| StoreError::EventConflict
)
}
}