blob: efc9c7f3ed7c61e2588e3bde84c9b87321241908 [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 futures_util::future::BoxFuture;
use maka_agent_facts::event::CommitError;
use maka_agent_facts::event::EventWrite;
use maka_agent_facts::event::Fact;
use maka_agent_facts::event::Invocation;
use maka_agent_facts::event::InvocationOutcome;
use maka_agent_facts::event::RuntimeEvent;
use maka_agent_facts::executor::Output;
use maka_agent_ports::executor::Error;
use maka_agent_ports::executor::Outcome;
use maka_agent_ports::executor::OutputSink;
use maka_event_log::EventLog;
use std::{collections::HashSet, sync::Arc};
use tokio::sync::Mutex;
use tokio_util::sync::CancellationToken;
pub(super) struct Recorder {
log: Arc<EventLog>,
invocation: Invocation,
cancellation: CancellationToken,
state: Mutex<State>,
}
#[derive(Default)]
struct State {
closed: bool,
calls: HashSet<String>,
pending: HashSet<String>,
failure: Option<CommitError>,
}
impl Recorder {
pub fn new(
log: Arc<EventLog>,
invocation: Invocation,
cancellation: CancellationToken,
) -> Self {
Self {
log,
invocation,
cancellation,
state: Mutex::default(),
}
}
pub async fn finish(
&self,
result: &Result<Outcome, Error>,
outcome: &InvocationOutcome,
) -> Result<(), crate::RunError> {
// Drain accepted writes before sealing, then reject even retained sinks.
let mut state = self.state.lock().await;
state.closed = true;
if let Some(error) = state.failure.take() {
return Err(error.into());
}
let mut facts = vec![];
if let (InvocationOutcome::Completed, Ok(Outcome::Completed { text })) = (&outcome, result)
{
facts.push(Fact::ExecutorCompleted { text: text.clone() });
}
facts.push(Fact::InvocationEnded {
outcome: outcome.clone(),
});
let writes = facts
.into_iter()
.map(|fact| EventWrite::plain(RuntimeEvent::new(self.invocation.clone(), fact)))
.collect::<Result<Vec<_>, _>>()?;
self.log.append_batch(&writes).await?;
Ok(())
}
}
impl OutputSink for Recorder {
fn emit(&self, output: Output) -> BoxFuture<'_, Result<(), Error>> {
Box::pin(async move {
let mut state = self.state.lock().await;
if state.closed || self.cancellation.is_cancelled() {
return Err(Error::Cancelled);
}
output
.validate()
.map_err(|reason| Error::Invalid(reason.into()))?;
match &output {
Output::ToolStart { tool_call_id, .. } if state.calls.contains(tool_call_id) => {
return Err(Error::Invalid("duplicate tool activity ID".into()));
}
Output::ToolProgress { tool_call_id, .. }
| Output::ToolResult { tool_call_id, .. }
if !state.pending.contains(tool_call_id) =>
{
return Err(Error::Invalid(
"external tool activity has no pending start".into(),
));
}
_ => {}
}
let event = EventWrite::plain(RuntimeEvent::new(
self.invocation.clone(),
Fact::ExecutorObserved {
output: output.clone(),
},
))
.map_err(|error| Error::Invalid(error.to_string()))?;
if let Err(error) = self.log.append(&event).await {
let message = error.to_string();
state.failure = Some(error);
self.cancellation.cancel();
return Err(Error::Persistence(message));
}
match output {
Output::ToolStart { tool_call_id, .. } => {
state.calls.insert(tool_call_id.clone());
state.pending.insert(tool_call_id);
}
Output::ToolResult { tool_call_id, .. } => {
state.pending.remove(&tool_call_id);
}
_ => {}
}
Ok(())
})
}
}