blob: 333b5e331f2d0a31b068580e343f1217462551d0 [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.
*/
//! Pure, disposable presentation of committed invocation facts. Model context is
//! a separate projection; retaining interrupted text here never accepts it there.
mod compact;
mod executor;
mod import;
mod message;
pub mod navigation;
pub mod shell;
mod step;
mod tools;
mod user;
use maka_agent_facts::event::Fact;
use maka_agent_facts::event::Invocation;
use maka_agent_facts::event::InvocationInput;
use maka_agent_facts::event::StoredEvent;
use maka_agent_facts::history::RowPosition;
use maka_agent_facts::tool_output::ToolOutput;
use message::timestamp;
pub use message::tool_message_id;
pub use message::{
Content, Hidden, Message, Thinking, ToolContent, ToolMetadata, TurnState, Visible,
};
#[derive(Debug, thiserror::Error)]
pub enum ProjectionError {
#[error("invalid presentation fact: {0}")]
Invalid(&'static str),
#[error("presentation text exceeds its byte limit")]
TooLarge,
#[error("presentation timestamp or sequence exceeds the wire range")]
OutOfRange,
#[error("unsupported presentation content: {0}")]
Unsupported(&'static str),
}
#[derive(Debug, PartialEq)]
pub struct Row {
/// Original emitting fact and its full row ordinal, before UI filtering.
pub position: RowPosition,
pub message: Message,
}
enum State {
Vacant,
Compact(Invocation),
Active {
invocation: Invocation,
step: Option<Box<step::Step>>,
},
Ended,
Poisoned,
}
pub struct InvocationView {
executor: Option<executor::Execution>,
state: State,
last_sequence: u64,
tools: tools::Tools,
summary_step: Option<String>,
summary_start: Option<compact::SummaryStart>,
}
impl Default for InvocationView {
fn default() -> Self {
Self {
executor: None,
state: State::Vacant,
last_sequence: 0,
tools: tools::Tools::new(),
summary_step: None,
summary_start: None,
}
}
}
impl InvocationView {
pub fn new() -> Self {
Self::default()
}
/// Input is one invocation's strictly ordered, committed facts, with gaps
/// permitted for other sessions. Never feed the same fact twice.
pub fn push(&mut self, stored: &StoredEvent) -> Result<Vec<Row>, ProjectionError> {
self.push_with_tool_output(stored, None)
}
/// Successful T2 requires its caller-verified raw payload. Resolution and
/// identity/digest verification belong to the durable store, not this view.
pub fn push_with_tool_output(
&mut self,
stored: &StoredEvent,
resolved: Option<&ToolOutput>,
) -> Result<Vec<Row>, ProjectionError> {
let result = self.apply(stored, resolved);
if result.is_err() {
self.state = State::Poisoned;
}
result
}
fn apply(
&mut self,
stored: &StoredEvent,
resolved: Option<&ToolOutput>,
) -> Result<Vec<Row>, ProjectionError> {
let succeeds = matches!(
stored.event.fact,
Fact::ToolSettled {
outcome: maka_agent_facts::event::ToolOutcome::Succeeded { .. },
..
}
);
if succeeds != resolved.is_some() {
return Err(ProjectionError::Invalid(
"tool payload does not match fact kind",
));
}
if stored.sequence <= self.last_sequence {
return Err(ProjectionError::Invalid("non-increasing log sequence"));
}
let event = &stored.event;
let ts = timestamp(event)?;
RowPosition {
source_sequence: stored.sequence,
item_index: 0,
}
.validate()
.map_err(|_| ProjectionError::OutOfRange)?;
if let Some(messages) = self.compact(event, ts)? {
self.last_sequence = stored.sequence;
return rows(stored.sequence, messages);
}
let mut messages = Vec::new();
match &event.fact {
Fact::MessageImported { record, .. } => {
if !matches!(self.state, State::Vacant) {
return Err(ProjectionError::Invalid("import inside execution"));
}
messages.extend(import::project(event, record, ts)?);
self.state = State::Ended;
}
Fact::InvocationOpened { input, .. } => {
if !matches!(self.state, State::Vacant) {
return Err(ProjectionError::Invalid("duplicate invocation opening"));
}
if let InvocationInput::Message {
content,
source_messages,
..
} = input
{
if source_messages
.iter()
.any(|source| source.message.origin.is_some())
{
for source in source_messages {
messages.push(user::project(
&source.message.message_id,
&event.invocation.turn_id,
ts,
&source.message.content,
source.message.origin.as_ref(),
)?);
}
} else {
messages.push(user::project(
if source_messages.len() == 1 {
&source_messages[0].message.message_id
} else {
&event.id
},
&event.invocation.turn_id,
ts,
content,
None,
)?);
}
} else if input.inherited_claim().is_none() {
return Err(ProjectionError::Unsupported("Code invocation"));
}
messages.push(Message {
id: format!("{}:state", event.id),
turn_id: event.invocation.turn_id.clone(),
ts,
content: Content::TurnState {
state: TurnState::Running,
},
});
self.state = State::Active {
invocation: event.invocation.clone(),
step: None,
};
}
fact => {
let State::Active { invocation, step } = &mut self.state else {
return Err(ProjectionError::Invalid("fact outside active invocation"));
};
if *invocation != event.invocation {
return Err(ProjectionError::Invalid("invocation identity changed"));
}
match fact {
Fact::ExecutorStarted { binding, .. } => {
if step.is_some() || self.executor.is_some() {
return Err(ProjectionError::Invalid("overlapping execution backends"));
}
self.executor = Some(executor::Execution::new(event, ts, binding));
}
Fact::ExecutorObserved { output } => {
messages.extend(
self.executor
.as_mut()
.ok_or(ProjectionError::Invalid("executor output without start"))?
.observe(event, ts, output)?,
);
}
Fact::ExecutorCompleted { text } => {
messages.extend(
self.executor
.as_mut()
.ok_or(ProjectionError::Invalid(
"executor completion without start",
))?
.complete(event, ts, text)?,
);
}
Fact::MessageSteered { message, .. } => {
if step.is_some() {
return Err(ProjectionError::Invalid(
"steering inside a model request",
));
}
messages.push(user::project(
&message.message_id,
&event.invocation.turn_id,
ts,
&message.content,
message.origin.as_ref(),
)?);
}
Fact::ModelRequested {
step_id,
model_id,
item_acceptance,
..
} => {
if step.is_some() {
return Err(ProjectionError::Invalid("overlapping model steps"));
}
*step = Some(Box::new(step::Step::new(
step_id.clone(),
model_id.clone(),
*item_acceptance,
)));
}
Fact::ModelObserved {
step_id,
event: observation,
} => {
let step = step
.as_mut()
.ok_or(ProjectionError::Invalid("observation without request"))?;
step.check_id(step_id)?;
step.observe(event, ts, observation)?;
if step.item_acceptance {
match observation {
maka_agent_facts::model::ModelEvent::PartFinished {
id, ..
} => {
messages.extend(step.deliver_text(id));
}
maka_agent_facts::model::ModelEvent::Source(_)
| maka_agent_facts::model::ModelEvent::ToolCall(_)
| maka_agent_facts::model::ModelEvent::ProviderToolResult {
..
} => {
if let Some((index, part)) = step.accepted_tool() {
messages.extend(self.tools.accept_item(
event,
ts,
step_id,
index,
part,
step.anchor(),
step.model(),
)?);
}
}
_ => {}
}
}
}
Fact::ModelCompleted { step_id, output } => {
let completed = step
.take()
.ok_or(ProjectionError::Invalid("completion without request"))?;
completed.check_id(step_id)?;
let streaming = completed.item_acceptance;
let text = completed.complete(output)?;
if streaming {
messages.extend(text);
} else {
messages.extend(self.tools.accept(event, ts, step_id, output, text)?);
}
if let (Some(input), Some(output_tokens)) =
(output.usage.input_tokens, output.usage.output_tokens)
{
messages.push(Message {
id: event.id.clone(),
turn_id: invocation.turn_id.clone(),
ts,
content: Content::TokenUsage {
input,
output: output_tokens,
cache_read: output.usage.cache_read_tokens,
cache_creation: output.usage.cache_write_tokens,
reasoning: output.usage.reasoning_tokens,
},
});
}
}
Fact::ModelInterrupted {
step_id,
status,
diagnostic,
..
} => {
let interrupted = step
.take()
.ok_or(ProjectionError::Invalid("interruption without request"))?;
interrupted.check_id(step_id)?;
messages.extend(interrupted.partial(
*status != maka_agent_facts::event::ModelInterruption::Cancelled,
));
if let Some(message) = diagnostic
&& *status != maka_agent_facts::event::ModelInterruption::Cancelled
{
messages.push(Message {
id: event.id.clone(),
turn_id: invocation.turn_id.clone(),
ts,
content: Content::ModelInterruption {
message: message.clone(),
},
});
}
}
Fact::InvocationEnded { outcome } => {
if let Some(executor) = self.executor.take() {
messages.extend(executor.finish(event, ts));
}
if let Some(unfinished) = step.take() {
if matches!(
outcome,
maka_agent_facts::event::InvocationOutcome::Completed
| maka_agent_facts::event::InvocationOutcome::HandoffPaused { .. }
) {
return Err(ProjectionError::Invalid(
"completion with unfinished model",
));
}
// Failure seals observation evidence, not accepted
// model history. Never invent a completed response.
messages.extend(unfinished.partial(matches!(
outcome,
maka_agent_facts::event::InvocationOutcome::Failed { .. }
)));
}
if let Some(state) = TurnState::from_outcome(outcome, ts) {
messages.push(Message {
id: event.id.clone(),
turn_id: invocation.turn_id.clone(),
ts,
content: Content::TurnState { state },
});
}
self.state = State::Ended;
}
Fact::ToolDispatched { .. }
| Fact::ToolSettled { .. }
| Fact::ToolRejected { .. } => {
messages.extend(self.tools.boundary(event, ts, resolved)?);
}
Fact::InvocationOpened { .. } | Fact::MessageImported { .. } => unreachable!(),
Fact::ContextCheckpointRecorded { .. }
| Fact::ToolResultArchived { .. }
| Fact::ToolOperationObserved { .. }
| Fact::ToolNotified { .. } => {}
}
}
}
self.last_sequence = stored.sequence;
rows(stored.sequence, messages)
}
/// Frozen caller-owned copies of observed text, including closed parts whose
/// model completion has not committed. These are overlay, not durable rows.
pub fn overlay(&self) -> Vec<Message> {
if let Some(executor) = &self.executor {
return executor.overlay();
}
match &self.state {
State::Active {
step: Some(step), ..
} => step.partial(false),
_ => Vec::new(),
}
}
}
fn rows(source_sequence: u64, messages: Vec<Message>) -> Result<Vec<Row>, ProjectionError> {
messages
.into_iter()
.enumerate()
.map(|(index, message)| {
let position = RowPosition {
source_sequence,
item_index: u64::try_from(index).map_err(|_| ProjectionError::OutOfRange)?,
};
position
.validate()
.map_err(|_| ProjectionError::OutOfRange)?;
Ok(Row { position, message })
})
.collect()
}