blob: b2e1b911216aedaae81ab4b8dfb66b841e77a3ee [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.
*/
//! Streaming acceptance proofs reuse ingress validation, without a second
//! durable projection. Rebuild at effect/terminal boundaries, never per delta.
use crate::StoreError;
use futures_util::TryStreamExt;
use maka_agent_facts::event::Fact;
use maka_agent_facts::event::RuntimeEvent;
use maka_agent_facts::model::assembly::StepBuilder;
use sqlx::SqliteConnection;
pub(crate) async fn read(
connection: &mut SqliteConnection,
invocation: &str,
step: &str,
) -> Result<Option<StepBuilder>, StoreError> {
let enabled: bool = sqlx::query_scalar("SELECT EXISTS(SELECT 1 FROM runtime_events WHERE invocation_id=? AND operation_id=? AND kind='model_requested' AND json_extract(event_json,'$.fact.item_acceptance')=1)")
.bind(invocation).bind(step).fetch_one(&mut *connection).await?;
if !enabled {
return Ok(None);
}
let mut builder = StepBuilder::for_step(step).map_err(invalid)?;
let mut rows = sqlx::query_scalar::<_, String>("SELECT event_json FROM runtime_events WHERE invocation_id=? AND kind='model_observed' AND CAST(json_extract(event_json,'$.fact.step_id') AS TEXT)=? ORDER BY sequence")
.bind(invocation).bind(step).fetch(connection);
while let Some(json) = rows.try_next().await? {
let event: RuntimeEvent = serde_json::from_str(&json)?;
let Fact::ModelObserved { event, .. } = event.fact else {
unreachable!()
};
builder.push(event).map_err(invalid)?;
}
Ok(Some(builder))
}
pub(crate) async fn validate(
connection: &mut SqliteConnection,
event: &RuntimeEvent,
) -> Result<(), StoreError> {
if let Fact::ModelRequested {
item_acceptance: true,
purpose,
..
} = &event.fact
&& *purpose != maka_agent_facts::context::ModelPurpose::Main
{
return Err(StoreError::InvalidTransition(
"item acceptance is only valid for main requests".into(),
));
}
let step = match &event.fact {
Fact::ModelCompleted { step_id, .. } | Fact::ModelInterrupted { step_id, .. } => step_id,
_ => return Ok(()),
};
if let Some(builder) = read(connection, &event.invocation.invocation_id, step).await?
&& let Fact::ModelCompleted { output, .. } = &event.fact
&& builder.finish().map_err(invalid)? != *output
{
return Err(StoreError::InvalidTransition(
"model completion differs from accepted observations".into(),
));
}
Ok(())
}
pub(crate) async fn project(
connection: &mut SqliteConnection,
invocation: maka_agent_facts::event::Invocation,
step_id: String,
sequence: u64,
event_id: String,
max_bytes: usize,
) -> Result<crate::context::AcceptedModelStep, StoreError> {
use maka_agent_facts::model::ModelEvent;
let mut builder = StepBuilder::for_step(&step_id).map_err(invalid)?;
let mut items = Vec::new();
let mut scratch = 0usize;
let mut rows = sqlx::query_as::<_, (i64, String)>("SELECT sequence,event_json FROM runtime_events WHERE invocation_id=?1 AND kind='model_observed' AND CAST(json_extract(event_json,'$.fact.step_id') AS TEXT)=?2 AND sequence<?3 ORDER BY sequence")
.bind(&invocation.invocation_id).bind(&step_id).bind(sequence as i64).fetch(connection);
while let Some((position, json)) = rows.try_next().await? {
let event: RuntimeEvent = serde_json::from_str(&json)?;
let Fact::ModelObserved {
event: observation, ..
} = event.fact
else {
unreachable!()
};
// Count accumulated content, not the repeated per-token JSON envelope.
scratch = scratch.saturating_add(match &observation {
ModelEvent::PartDelta {
text,
provider_options,
..
} => {
text.len()
+ provider_options
.as_ref()
.map_or(0, |value| value.to_string().len())
}
ModelEvent::ToolInputStarted { .. }
| ModelEvent::ToolInputDelta { .. }
| ModelEvent::ToolInputFinished { .. }
| ModelEvent::ResponseMetadata { .. }
| ModelEvent::Finished { .. } => 0,
_ => serde_json::to_vec(&observation)?.len(),
});
if scratch > max_bytes {
return Err(StoreError::PrefixTooLarge);
}
builder.push(observation).map_err(invalid)?;
if let Some((index, part)) = builder.accepted_item() {
items.push(crate::context::AcceptedModelItem {
sequence: crate::sequence_number(position)?,
event_id: event.id,
index,
part: part.clone(),
});
}
}
drop(rows);
items.sort_by_key(|item| item.index);
Ok(crate::context::AcceptedModelStep {
sequence,
event_id,
invocation,
step_id,
items,
})
}
fn invalid(error: impl std::fmt::Display) -> StoreError {
StoreError::InvalidTransition(format!("invalid model item acceptance: {error}"))
}
// One selection rule for recovery, context fences and archive causality.
macro_rules! accepted_calls {
() => { "accepted_calls AS NOT MATERIALIZED (
SELECT m.invocation_id, m.operation_id AS step_id, m.sequence, m.event_id,
json_extract(p.value,'$.call') AS call
FROM runtime_events m, json_each(m.event_json,'$.fact.output.parts') p
WHERE m.kind='model_completed' AND json_extract(p.value,'$.kind')='tool_call'
AND NOT EXISTS(SELECT 1 FROM runtime_events r WHERE r.invocation_id=m.invocation_id
AND r.operation_id=m.operation_id AND r.kind='model_requested'
AND json_extract(r.event_json,'$.fact.item_acceptance')=1)
UNION ALL
SELECT o.invocation_id, r.operation_id, o.sequence, o.event_id,
json_extract(o.event_json,'$.fact.event.data')
FROM runtime_events r JOIN runtime_events o ON o.invocation_id=r.invocation_id
AND o.kind='model_observed' AND CAST(json_extract(o.event_json,'$.fact.step_id') AS TEXT)=r.operation_id
WHERE r.kind='model_requested' AND json_extract(r.event_json,'$.fact.item_acceptance')=1
AND json_extract(o.event_json,'$.fact.event.kind')='tool_call'
)" };
}
pub(crate) use accepted_calls;