blob: 3e1fc7eaa947bf678a26ca2e15703ec0bec2302b [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.
*/
mod output;
use crate::{Engine, RunError, RunningInvocation};
use maka_agent_facts::event::EventWrite;
use maka_agent_facts::event::Fact;
use maka_agent_facts::event::InvocationInput;
use maka_agent_facts::event::InvocationOutcome;
use maka_agent_facts::event::RuntimeEvent;
use maka_agent_facts::execution::InvocationConfiguration;
use maka_agent_facts::message::RootSourceMessage;
use maka_agent_ports::executor::Backend;
use maka_agent_ports::executor::Error;
use maka_agent_ports::executor::Outcome;
use maka_agent_ports::executor::Request;
use std::sync::Arc;
use tokio_util::sync::CancellationToken;
pub struct ExecutorInput<B> {
pub request: Request,
pub binding: B,
pub configuration: InvocationConfiguration,
pub request_fingerprint: Option<String>,
pub source_messages: Vec<RootSourceMessage>,
}
impl Engine {
pub async fn start_executor<B: Backend>(
&self,
input: ExecutorInput<B>,
cancellation: CancellationToken,
) -> Result<RunningInvocation, RunError> {
if input.configuration.model.is_some() || input.configuration.cwd != input.request.cwd {
return Err(RunError::InvalidInput(
"executor cannot carry a model binding or mismatched workspace".into(),
));
}
maka_agent_facts::message::validate_sources(&input.request.content, &input.source_messages)
.map_err(|reason| RunError::InvalidInput(reason.into()))?;
let admitted = input
.binding
.admit_execution(input.request.clone())
.map_err(|error| RunError::InvalidInput(error.to_string()))?;
let identity = admitted.identity;
identity
.validate()
.map_err(|error| RunError::InvalidInput(error.into()))?;
let call = admitted.execution;
let inner = self.0.clone();
self.start_owned(
input.request.invocation.clone(),
Arc::default(),
None,
cancellation,
move |cancellation, admitted| async move {
if cancellation.is_cancelled() {
return Err(RunError::Cancelled);
}
let invocation = input.request.invocation;
// The durable backend identity precedes every external effect, in
// the same transaction as the invocation admission.
let writes = [
Fact::InvocationOpened {
configuration: Some(Box::new(input.configuration)),
input: InvocationInput::Message {
content: input.request.content,
request_fingerprint: input.request_fingerprint,
source_messages: input.source_messages,
},
},
Fact::ExecutorStarted {
binding: identity,
settings: input.request.settings,
},
]
.into_iter()
.map(|fact| EventWrite::plain(RuntimeEvent::new(invocation.clone(), fact)))
.collect::<Result<Vec<_>, _>>()?;
inner.log.append_batch(&writes).await?;
let _ = admitted.send(());
let output = Arc::new(output::Recorder::new(
inner.log.clone(),
invocation.clone(),
cancellation.clone(),
));
let settlement = call.execute(output.clone(), cancellation.clone()).await;
if matches!(&settlement.result, Err(Error::CleanupUnconfirmed)) {
crate::cleanup::unconfirmed();
}
let outcome = outcome(&settlement.result, &cancellation);
let committed = output.finish(&settlement.result, &outcome).await;
drop(settlement); // the terminal fact owns the lease through its commit
committed?;
match outcome {
InvocationOutcome::Completed => Ok(invocation),
InvocationOutcome::Cancelled { .. } => Err(RunError::Cancelled),
InvocationOutcome::Failed { message, .. } => Err(RunError::Internal(
message.unwrap_or_else(|| "external executor failed".into()),
)),
_ => unreachable!("executor outcomes"),
}
},
)
.await
}
}
fn outcome(result: &Result<Outcome, Error>, cancellation: &CancellationToken) -> InvocationOutcome {
match result {
Ok(Outcome::Completed { .. }) if !cancellation.is_cancelled() => {
InvocationOutcome::Completed
}
Err(Error::Cancelled) | Ok(Outcome::Completed { .. }) => InvocationOutcome::Cancelled {
source: "runtime_cancellation".into(),
},
Ok(Outcome::Cancelled { .. }) => InvocationOutcome::Cancelled {
source: "executor_provider".into(),
},
Err(Error::Retired) => InvocationOutcome::Cancelled {
source: "executor_retired".into(),
},
Ok(Outcome::Failed { message, .. }) => InvocationOutcome::Failed {
model_failure: None,
class: "executor".into(),
message: Some(message.chars().take(2048).collect()),
},
Err(error) => InvocationOutcome::Failed {
model_failure: None,
class: if matches!(error, Error::CleanupUnconfirmed | Error::Unknown) {
"outcome_unknown"
} else {
"executor"
}
.into(),
message: Some(error.to_string().chars().take(2048).collect()),
},
}
}