blob: 589501812631c255ebd522a4cf11f91fab75e6df [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,
turns::{InvocationState, TurnBoundary},
};
use maka_agent_facts::model::TextKind;
use sqlx::Row;
/// Active stream identity and replay start, without reading accumulated text.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AssistantStreamSeed {
pub start_sequence: u64,
pub step_id: String,
pub part_id: String,
/// Canonical PartStarted event ID; provider IDs may repeat across steps.
pub message_id: String,
pub text_kind: TextKind,
}
pub(super) async fn read(
connection: &mut sqlx::SqliteConnection,
root: Option<&TurnBoundary>,
) -> Result<Vec<AssistantStreamSeed>, StoreError> {
let Some(root) = root else {
return Ok(Vec::new());
};
if matches!(root.state, InvocationState::Ended { .. })
|| matches!(
root.input,
maka_agent_facts::input::InvocationInput::ContextCompact { .. }
)
{
return Ok(Vec::new());
}
let external: Option<(i64, String)> = sqlx::query_as(
"SELECT sequence, event_id FROM runtime_events started WHERE invocation_id = ?1 AND kind = 'executor_started'
AND NOT EXISTS(SELECT 1 FROM runtime_events done WHERE done.invocation_id = started.invocation_id AND done.kind = 'executor_completed')"
).bind(&root.invocation.invocation_id).fetch_optional(&mut *connection).await?;
if let Some((sequence, message)) = external {
let sequence = crate::sequence_number(sequence)?;
return Ok([TextKind::Text, TextKind::Thinking]
.into_iter()
.map(|text_kind| AssistantStreamSeed {
start_sequence: sequence,
step_id: root.invocation.invocation_id.clone(),
part_id: if text_kind == TextKind::Text {
"text"
} else {
"thinking"
}
.into(),
message_id: message.clone(),
text_kind,
})
.collect());
}
// Select only identities. Neither finished output bodies nor accumulated text
// cross the SQL boundary. The invocation_sequence index scopes scans.
let rows = sqlx::query(
"WITH seeds AS MATERIALIZED (
SELECT s.sequence, json_extract(s.event_json, '$.fact.step_id') AS step_id, s.event_id,
json_extract(s.event_json, '$.fact.event.data.id') AS part_id,
json_extract(s.event_json, '$.fact.event.data.text_kind') AS text_kind
FROM runtime_events s WHERE s.invocation_id = ?1 AND s.kind = 'model_observed'
AND 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 = s.invocation_id
AND request.operation_id = json_extract(s.event_json, '$.fact.step_id')
AND json_extract(opening.event_json, '$.fact.input.kind') IN ('message', 'continuation', 'handoff')
AND json_extract(request.event_json, '$.fact.purpose') = 'main')
AND json_extract(s.event_json, '$.fact.event.kind') = 'part_started'
AND NOT EXISTS (
SELECT 1 FROM runtime_events e
WHERE e.invocation_id = s.invocation_id
AND json_extract(e.event_json, '$.fact.step_id') = json_extract(s.event_json, '$.fact.step_id')
AND (e.kind IN ('model_interrupted', 'model_completed') OR
(e.sequence > s.sequence AND e.kind = 'model_observed'
AND json_extract(e.event_json, '$.fact.event.kind') = 'part_started'
AND json_extract(e.event_json, '$.fact.event.data.id') =
json_extract(s.event_json, '$.fact.event.data.id'))))
ORDER BY s.sequence)
SELECT sequence, step_id, event_id, part_id, text_kind
FROM seeds ORDER BY sequence",
).bind(&root.invocation.invocation_id).fetch_all(&mut *connection).await?;
let mut result = Vec::new();
for row in rows {
let sequence: i64 = row.try_get(0)?;
let step_id: String = row.try_get(1)?;
let part_id: String = row.try_get(3)?;
let text_kind = match row.try_get::<String, _>(4)?.as_str() {
"text" => TextKind::Text,
"thinking" => TextKind::Thinking,
_ => {
return Err(StoreError::InvalidTransition(
"invalid stored text kind".into(),
));
}
};
result.push(AssistantStreamSeed {
start_sequence: crate::sequence_number(sequence)?,
step_id,
part_id,
message_id: row.try_get(2)?,
text_kind,
});
}
Ok(result)
}
/// Display status from the same committed snapshot as the session observation.
pub(super) async fn compacting(
connection: &mut sqlx::SqliteConnection,
root: Option<&TurnBoundary>,
) -> Result<bool, StoreError> {
let Some(root) = root else {
return Ok(false);
};
if matches!(root.state, InvocationState::Ended { .. }) {
return Ok(false);
}
Ok(sqlx::query_scalar::<_, bool>(
"SELECT EXISTS(SELECT 1 FROM runtime_events request
WHERE request.invocation_id = ?1 AND request.kind = 'model_requested'
AND json_extract(request.event_json, '$.fact.purpose') = 'summary'
AND NOT EXISTS(SELECT 1 FROM runtime_events done
WHERE done.invocation_id = request.invocation_id
AND done.operation_id = request.operation_id
AND done.kind IN ('model_completed', 'model_interrupted')))",
)
.bind(&root.invocation.invocation_id)
.fetch_one(connection)
.await?)
}