blob: e40e961efa044b1d46efa2a0e8fd7ddc3309dfc7 [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 super::*;
use maka_agent_facts::event::InvocationOutcome;
use maka_agent_ports::execution::{Receipt, Submit};
use maka_assistant::plan::{Execution, Settlement};
use maka_event_log::{
message_admissions::{PendingExecution, PendingMessageAdmission},
turns::InvocationState,
};
pub(super) fn invocation(request: &Submit) -> Invocation {
let id = hex::encode(Sha256::digest(request.operation_id.as_bytes()));
Invocation {
session_id: request.session_id.clone(),
turn_id: format!("t_{id}"),
run_id: format!("r_{id}"),
invocation_id: format!("i_{id}"),
}
}
fn admission(
owner: &str,
execution: &Execution<MessageBasis>,
) -> Result<PendingMessageAdmission, String> {
let invocation = invocation(&execution.request);
let message = execution.request.content.clone();
let mut basis = execution.grant.clone();
basis.fingerprint = identity(&execution.request)?;
Ok(PendingMessageAdmission {
execution: PendingExecution {
owner: owner.into(),
basis: serde_json::to_value(basis).map_err(reason)?,
},
source: RootSourceMessage {
message: DeliveredMessage {
origin: None,
message_id: invocation.invocation_id.replacen("i_", "m_", 1),
submitted_content_digest: message.content_digest().map_err(reason)?,
content: message.clone(),
},
unprepared_content: message,
submitted_placement: Placement::CurrentTurn,
disposition: MessageDisposition::TurnStarted,
submitted_intent: None,
},
invocation,
steering_invocation: None,
required_tools: Default::default(),
admitted_at: 0,
})
}
pub(super) fn receipt(execution: &Execution<MessageBasis>) -> Result<Receipt, String> {
let invocation = invocation(&execution.request);
Ok(Receipt {
message_id: invocation.invocation_id.replacen("i_", "m_", 1),
invocation,
content_digest: execution.request.digest().map_err(reason)?,
})
}
fn active(snapshot: &Snapshot<MessageBasis>) -> bool {
snapshot.execution.as_ref().is_some_and(|execution| {
matches!(
execution.phase,
Phase::AwaitingAdmission | Phase::Active { .. }
)
})
}
impl Owner {
pub(in crate::owner) fn start_plans(self: &Arc<Self>) {
let owner = self.clone();
self.producers.spawn(async move {
let mut changes = owner.store.subscribe_commits();
let mut previous_error = None;
loop {
changes.borrow_and_update();
let Some(producer) = owner.producer().await else {
return;
};
let monitor = match owner.tick_plans().await {
Ok(monitor) => {
previous_error = None;
monitor
}
Err(error) => {
owner.plans.awake.store(true, Ordering::Release);
if previous_error.as_ref() != Some(&error) {
eprintln!("Plan: {error}");
}
previous_error = Some(error);
true
}
};
drop(producer);
tokio::select! {
biased;
() = owner.stop.cancelled() => return,
() = owner.plans.wake.notified() => {},
changed = changes.changed(), if monitor => if changed.is_err() { return; },
() = tokio::time::sleep(Duration::from_secs(5)), if monitor => {},
}
}
});
}
async fn tick_plans(self: &Arc<Self>) -> Result<bool, String> {
let _gate = self.plans.gate.lock().await;
let sessions = Repository::<MessageBasis>::sessions(self.plans.storage.as_ref(), ENTRY)
.await
.map_err(reason)?;
let mut monitor = false;
for session in sessions {
if self.stop.is_cancelled() {
break;
}
let repo = self.plans.repository(&session)?;
let snapshot = repo.current().await.map_err(reason)?;
if active(&snapshot) {
self.tick_plan(&session, snapshot).await?;
}
monitor |= active(&repo.current().await.map_err(reason)?);
}
self.plans.awake.store(monitor, Ordering::Release);
Ok(monitor)
}
async fn tick_plan(
self: &Arc<Self>,
session: &str,
mut snapshot: Snapshot<MessageBasis>,
) -> Result<(), String> {
let execution = snapshot
.execution
.as_ref()
.ok_or("plan_execution_missing")?;
let original = invocation(&execution.request);
let request = admission(&self.prefix, execution)?;
if let Some(boundary) = self
.store
.turn_boundary(session, &original.turn_id)
.await
.map_err(reason)?
{
if boundary.root_invocation() != &original {
return Err("plan_execution_identity_changed".into());
}
if matches!(execution.phase, Phase::AwaitingAdmission) {
snapshot = self
.plans
.advance(
session,
&snapshot,
Command::Accept {
execution_id: execution.id.clone(),
receipt: receipt(execution)?,
},
)
.await?;
}
let execution = snapshot
.execution
.as_ref()
.ok_or("plan_execution_missing")?;
let runs = self.runs.lock().await;
if let Some(control) = runs
.controls
.get(session)
.filter(|control| control.invocation.turn_id == original.turn_id)
{
if execution.cancellation.is_some() {
control.cancellation.cancel();
}
// Logical completion never outruns the original worker's physical cleanup.
return Ok(());
}
drop(runs);
let InvocationState::Ended { outcome, .. } = boundary.state else {
return Ok(());
};
let outcome = match outcome {
InvocationOutcome::Completed => Settlement::Completed,
InvocationOutcome::Failed { class, message, .. } => Settlement::Interrupted { reason: bounded(&message.unwrap_or(class)) },
InvocationOutcome::Cancelled { source } => Settlement::Interrupted { reason: bounded(&format!("Execution cancelled: {source}")) },
InvocationOutcome::HandoffPaused { .. } => Settlement::Interrupted { reason: "Original execution is paused; resume the Session before approving another attempt".into() },
_ => Settlement::Interrupted { reason: "Execution ended without Plan completion".into() },
};
self.plans
.advance(
session,
&snapshot,
Command::Settle {
execution_id: execution.id.clone(),
invocation: original,
outcome,
},
)
.await?;
return Ok(());
}
if matches!(execution.phase, Phase::Active { .. }) {
return Err("plan_original_execution_unavailable".into());
}
let message = &request.source.message.message_id;
let cancelled = self
.store
.message_cancelled(session, message)
.await
.map_err(reason)?;
let accepted = self
.store
.message_admission(session, message)
.await
.map_err(reason)?
.is_some();
if cancelled || (execution.cancellation.is_some() && !accepted) {
// All dispatches share this gate; the original admission has settled and is absent.
self.plans
.advance(
session,
&snapshot,
Command::Reject {
execution_id: execution.id.clone(),
request_digest: execution.request.digest().map_err(reason)?,
reason: "Original Plan message cancelled before execution".into(),
},
)
.await?;
return Ok(());
}
if !accepted {
if let Err(error) = self.validate_background(&execution.grant).await {
self.plans
.advance(
session,
&snapshot,
Command::Reject {
execution_id: execution.id.clone(),
request_digest: execution.request.digest().map_err(reason)?,
reason: bounded(&error),
},
)
.await?;
return Ok(());
}
if !execution.dispatched {
self.plans
.advance(
session,
&snapshot,
Command::Dispatch {
execution_id: execution.id.clone(),
},
)
.await?;
}
}
// Accepted cancelled inputs are drained by the original queue's normal preparation check.
self.admit_notification(&request).await?;
Ok(())
}
}
fn bounded(value: &str) -> String {
let value = value.replace('\0', "");
let value = value[..value.floor_char_boundary(value.len().min(1024))].trim();
if value.is_empty() {
"Plan execution interrupted".into()
} else {
value.into()
}
}