blob: bea5929681b5a9572cae624bafa9ecb8dcbaa845 [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.
*/
//! Disposable identity lookup over immutable opening and steering facts.
use crate::{EventLog, StoreError, sequence_number};
use maka_agent_facts::event::Fact;
use maka_agent_facts::event::InvocationInput;
use maka_agent_facts::event::RuntimeEvent;
use maka_agent_facts::event::StoredEvent;
use maka_agent_facts::message::RootSourceMessage;
use sqlx::{Connection, SqliteConnection};
pub(crate) fn roots(event: &RuntimeEvent) -> &[RootSourceMessage] {
match &event.fact {
Fact::InvocationOpened {
input: InvocationInput::Message {
source_messages, ..
},
..
} => source_messages,
Fact::MessageSteered {
source: Some(source),
..
} => std::slice::from_ref(source.as_ref()),
_ => &[],
}
}
pub(crate) fn identities(event: &RuntimeEvent) -> impl Iterator<Item = &str> {
roots(event)
.iter()
.map(|source| source.message.message_id.as_str())
.chain(match &event.fact {
Fact::MessageSteered {
message,
source: None,
} => Some(message.message_id.as_str()),
_ => None,
})
}
pub(crate) async fn initialize(connection: &mut SqliteConnection) -> Result<(), StoreError> {
let populated: bool = sqlx::query_scalar("SELECT EXISTS(SELECT 1 FROM message_sources)")
.fetch_one(&mut *connection)
.await?;
if populated {
return Ok(());
}
let mut tx = connection.begin().await?;
sqlx::raw_sql(
"INSERT INTO message_sources
SELECT json_extract(event_json, '$.invocation.session_id'),
json_extract(event_json, '$.fact.message.message_id'), event_id
-- Match runtime_events while using the sparse steering identity index.
FROM event_log INDEXED BY message_steering_identity
WHERE kind = 'message_steered' AND invocation_id IS NOT NULL AND event_json IS NOT NULL;
INSERT INTO message_sources
SELECT json_extract(e.event_json, '$.invocation.session_id'),
json_extract(s.value, '$.message_id'), e.event_id
FROM runtime_events e, json_each(e.event_json, '$.fact.input.source_messages') s
WHERE e.kind = 'invocation_opened' AND json_extract(e.event_json, '$.fact.input.kind') = 'message'
AND json_array_length(e.event_json, '$.fact.input.source_messages') > 0;"
).execute(&mut *tx).await?;
tx.commit().await.map_err(StoreError::CommitUnknown)?;
Ok(())
}
pub(crate) async fn insert(
connection: &mut SqliteConnection,
event: &RuntimeEvent,
) -> Result<(), StoreError> {
for id in identities(event) {
sqlx::query("INSERT INTO message_sources VALUES (?, ?, ?)")
.bind(&event.invocation.session_id)
.bind(id)
.bind(&event.id)
.execute(&mut *connection)
.await?;
}
Ok(())
}
#[derive(Debug)]
pub struct RootMessageProof {
opening: StoredEvent,
index: usize,
}
impl RootMessageProof {
pub fn opening(&self) -> &StoredEvent {
&self.opening
}
pub fn source(&self) -> &RootSourceMessage {
&roots(&self.opening.event)[self.index]
}
}
impl EventLog {
/// Last admitted user-message opening in this session's owned history.
pub async fn latest_message_opening(
&self,
session_id: &str,
) -> Result<Option<StoredEvent>, StoreError> {
self.validate_root()?;
crate::sessions::validate_id(session_id)?;
let session = session_id.to_owned();
self.connection.read(move |connection| Box::pin(async move {
let row: Option<(i64, String)> = sqlx::query_as(
"WITH candidates AS (
SELECT sequence FROM (
SELECT e.sequence FROM runtime_events e
WHERE json_extract(e.event_json,'$.invocation.session_id')=?1 AND e.kind='invocation_opened' AND json_extract(e.event_json,'$.fact.input.kind')='message'
ORDER BY e.sequence DESC LIMIT 1)
UNION ALL
SELECT sequence FROM (
SELECT h.sequence FROM session_history_members h
JOIN runtime_events e ON e.sequence=h.sequence
WHERE h.session_id=?1 AND e.kind='invocation_opened' AND json_extract(e.event_json,'$.fact.input.kind')='message'
ORDER BY h.sequence DESC LIMIT 1)
UNION ALL
SELECT sequence FROM (
SELECT r.sequence FROM session_revision_sources r
JOIN runtime_events e ON e.sequence=r.sequence
WHERE r.session_id=?1 AND e.kind='invocation_opened' AND json_extract(e.event_json,'$.fact.input.kind')='message'
ORDER BY r.sequence DESC LIMIT 1))
SELECT e.sequence,e.event_json FROM runtime_events e
WHERE e.sequence=(SELECT MAX(sequence) FROM candidates)"
).bind(session).fetch_optional(connection).await?;
row.map(|(sequence, json)| decode_delivery(sequence, json)).transpose()
})).await
}
/// The earliest user-message opening in owned history, including inherited
/// prefixes. Later messages and successor invocations cannot become the first.
pub async fn first_message_opening(
&self,
session_id: &str,
) -> Result<Option<StoredEvent>, StoreError> {
self.validate_root()?;
crate::sessions::validate_id(session_id)?;
let session = session_id.to_owned();
self.connection
.run(move |connection| {
Box::pin(async move { first_message_opening(connection, &session).await })
})
.await
}
/// Resolve one exact root source from owned history. A Turn can contain
/// several queued messages; neither its aggregate nor a UI row is the input.
pub async fn editable_message(
&self,
session_id: &str,
turn_id: &str,
message_id: &str,
) -> Result<Option<maka_agent_facts::message::EditableMessage>, StoreError> {
self.validate_root()?;
for id in [session_id, turn_id, message_id] {
crate::sessions::validate_id(id)?;
}
let (session, turn, message) = (
session_id.to_owned(),
turn_id.to_owned(),
message_id.to_owned(),
);
self.connection.run(move |connection| Box::pin(async move {
let mut tx = connection.begin().await?;
let row: Option<(i64, String)> = sqlx::query_as(
"SELECT e.sequence, e.event_json
FROM session_message_sources s JOIN runtime_events e ON e.event_id=s.event_id
WHERE s.owner_session_id=?1 AND s.message_id=?2
AND (e.kind='invocation_opened' OR (e.kind='message_steered' AND json_type(e.event_json,'$.fact.source')='object'))
AND json_extract(e.event_json,'$.invocation.turn_id')=?3"
).bind(&session).bind(&message).bind(&turn).fetch_optional(&mut *tx).await?;
let Some((sequence, json)) = row else { return Ok(None); };
let proof = decode_root(sequence, json, &message)?;
if proof.opening.event.invocation.turn_id != turn {
return Err(invalid("source proof Turn changed"));
}
let result = editable(&mut tx, &session, &proof.opening, proof.source()).await?;
tx.commit().await?;
Ok(Some(result))
})).await
}
/// Resolve an original source identity without inferring ownership from UI rows.
pub async fn root_message(
&self,
session_id: &str,
message_id: &str,
) -> Result<Option<RootMessageProof>, StoreError> {
self.validate_root()?;
crate::sessions::validate_id(session_id)?;
crate::sessions::validate_id(message_id)?;
let session = session_id.to_owned();
let message = message_id.to_owned();
self.connection.run(move |connection| Box::pin(async move {
let row: Option<(i64, String)> = sqlx::query_as(
"SELECT e.sequence, e.event_json
FROM message_sources s JOIN runtime_events e ON e.event_id = s.event_id
WHERE s.session_id = ? AND s.message_id = ?
AND (e.kind='invocation_opened' OR (e.kind='message_steered' AND json_type(e.event_json,'$.fact.source')='object'))"
).bind(&session).bind(&message).fetch_optional(connection).await?;
let Some((sequence, json)) = row else { return Ok(None); };
let proof = decode_root(sequence, json, &message)?;
if proof.opening.event.invocation.session_id != session { return Err(invalid("source proof Session changed")); }
Ok(Some(proof))
})).await
}
}
fn decode_root(sequence: i64, json: String, message: &str) -> Result<RootMessageProof, StoreError> {
let opening = decode_delivery(sequence, json)?;
let index = roots(&opening.event)
.iter()
.position(|source| source.message.message_id == message)
.ok_or_else(|| invalid("source proof identity changed"))?;
Ok(RootMessageProof { opening, index })
}
fn decode_delivery(sequence: i64, json: String) -> Result<StoredEvent, StoreError> {
let event: RuntimeEvent = serde_json::from_str(&json)?;
match &event.fact {
Fact::InvocationOpened {
input:
InvocationInput::Message {
content,
source_messages,
..
},
..
} => maka_agent_facts::message::validate_sources(content, source_messages)
.map_err(invalid)?,
Fact::MessageSteered {
message,
source: Some(source),
} => {
maka_agent_facts::event::validate_steered_source(
message,
Some(source),
&event.invocation.session_id,
)
.map_err(invalid)?;
}
_ => return Err(invalid("source proof has no canonical input")),
}
for source in roots(&event) {
if let Some(intent) = &source.submitted_intent {
maka_agent_facts::input::validate_selection_session(
&intent.input_selection_sources,
&event.invocation.session_id,
)
.map_err(invalid)?;
}
}
Ok(StoredEvent {
sequence: sequence_number(sequence)?,
event,
})
}
async fn editable(
connection: &mut SqliteConnection,
session: &str,
opening: &StoredEvent,
source: &RootSourceMessage,
) -> Result<maka_agent_facts::message::EditableMessage, StoreError> {
let mut content = source.unprepared_content.clone();
if opening.event.invocation.session_id != session {
for attachment in content.attachments.iter_mut().flatten() {
if let maka_agent_facts::attachment::StorageRef::SessionFile {
session_id,
relative_path,
} = &attachment.storage_ref
{
attachment.storage_ref = crate::artifacts::history::resolve(
connection,
session,
session_id,
relative_path,
)
.await?
.ok_or_else(|| invalid("editable source Artifact is missing"))?;
}
}
}
Ok(maka_agent_facts::message::EditableMessage {
message_id: source.message.message_id.clone(),
turn_id: opening.event.invocation.turn_id.clone(),
content,
intent: source.submitted_intent.clone(),
})
}
fn invalid(message: &str) -> StoreError {
StoreError::InvalidTransition(message.into())
}
/// Shared with auxiliary admission, inside its write transaction.
pub(crate) async fn first_message_opening(
connection: &mut SqliteConnection,
session: &str,
) -> Result<Option<StoredEvent>, StoreError> {
let row: Option<(i64, String, String)> = sqlx::query_as(
"WITH candidates AS (
SELECT sequence FROM (
SELECT e.sequence FROM runtime_events e
WHERE e.event_json IS NOT NULL AND e.event_session=?1 AND ((e.kind='invocation_opened' AND json_extract(e.event_json,'$.fact.input.kind')='message') OR (e.kind='message_imported' AND json_extract(e.event_json,'$.fact.record.content.kind')='user'))
ORDER BY e.sequence ASC LIMIT 1)
UNION ALL
SELECT sequence FROM (
SELECT h.sequence FROM session_history_members h
JOIN runtime_events e ON e.sequence=h.sequence
WHERE h.session_id=?1 AND ((e.kind='invocation_opened' AND json_extract(e.event_json,'$.fact.input.kind')='message') OR (e.kind='message_imported' AND json_extract(e.event_json,'$.fact.record.content.kind')='user'))
ORDER BY h.sequence ASC LIMIT 1)
UNION ALL
SELECT sequence FROM (
SELECT r.sequence FROM session_revision_sources r
JOIN runtime_events e ON e.sequence=r.sequence
WHERE r.session_id=?1 AND ((e.kind='invocation_opened' AND json_extract(e.event_json,'$.fact.input.kind')='message') OR (e.kind='message_imported' AND json_extract(e.event_json,'$.fact.record.content.kind')='user'))
ORDER BY r.sequence ASC LIMIT 1))
SELECT e.sequence,e.event_json,e.kind FROM runtime_events e
WHERE e.sequence=(SELECT MIN(sequence) FROM candidates)"
).bind(session).fetch_optional(connection).await?;
match row {
Some((sequence, json, kind)) if kind == "invocation_opened" => {
decode_delivery(sequence, json).map(Some)
}
_ => Ok(None),
}
}