blob: 2e6aa0c35593b60d53af76e9e584d85c475750ee [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_facts::language::Language;
use maka_event_log::{
message_admissions::{PendingExecution, PendingMessageAdmission},
turns::InvocationState,
};
pub(super) fn identity(request: &maka_agent_ports::execution::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(
goal: &maka_goal::goal::Goal<MessageBasis>,
) -> Result<PendingMessageAdmission, String> {
let pending = goal.pending.as_ref().ok_or("goal_iteration_missing")?;
let invocation = identity(&pending.request);
let mut basis = goal.arm.grant.clone();
basis.fingerprint = hex::encode(Sha256::digest(
serde_json::to_vec(&pending.request).map_err(reason)?,
));
let message = pending.request.content.clone();
Ok(PendingMessageAdmission {
execution: PendingExecution {
owner: String::new(),
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(),
// Logical Goal identity has no wall-clock dependency; retries are byte-identical.
admitted_at: 0,
})
}
impl Owner {
pub(in crate::owner) fn start_goals(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_goals().await {
Ok(monitor) => {
previous_error = None;
monitor
}
Err(error) => {
owner.goals.awake.store(true, Ordering::Release);
if previous_error.as_ref() != Some(&error) {
eprintln!("Goal: {error}");
}
previous_error = Some(error);
true
}
};
drop(producer);
tokio::select! {
biased;
() = owner.stop.cancelled() => return,
() = owner.goals.wake.notified() => {},
changed = changes.changed(), if monitor => if changed.is_err() { return; },
() = tokio::time::sleep(Duration::from_secs(5)), if monitor => {},
}
}
});
}
async fn tick_goals(self: &Arc<Self>) -> Result<bool, String> {
let _gate = self.goals.gate.lock().await;
for saved in self.goals.repository.sessions().await.map_err(reason)? {
if self.stop.is_cancelled() {
break;
}
self.tick_goal(saved).await?;
}
let monitored = self.goals.repository.sessions().await.map_err(reason)?;
// Publish while the same gate still excludes Goal mutations. Otherwise
// an idle tick could overwrite a just-created Goal's retirement hint.
self.goals.awake.store(
monitored.iter().any(|saved| saved.goal.keeps_host_awake()),
Ordering::Release,
);
Ok(!monitored.is_empty())
}
async fn tick_goal(self: &Arc<Self>, mut saved: Saved<MessageBasis>) -> Result<(), String> {
let session = saved.goal.session_id.clone();
if let Some(pending) = &saved.goal.pending {
let original = identity(&pending.request);
let boundary = self
.store
.turn_boundary(&session, &original.turn_id)
.await
.map_err(reason)?;
if let Some(boundary) = boundary {
if boundary.root_invocation() != &original {
return Err("goal_execution_identity_changed".into());
}
// A terminal event does not certify physical cleanup. The original
// worker must release its control before the Goal can advance.
let runs = self.runs.lock().await;
if let Some(control) = runs
.controls
.get(&session)
.filter(|control| control.invocation.turn_id == original.turn_id)
{
if matches!(
saved.goal.status,
Status::Cancelled | Status::CancellationUnknown
) {
control.cancellation.cancel();
}
return Ok(());
}
drop(runs);
match boundary.state {
InvocationState::Ended {
outcome: InvocationOutcome::HandoffPaused { .. },
..
} => {
saved.goal.handoff_paused();
}
InvocationState::Ended { outcome, .. } => {
saved.goal.settled(outcome, self.goal_meter(&session).await);
}
_ => return Ok(()),
}
return self.goals.save(saved).await;
}
let mut request = admission(&saved.goal)?;
request.execution.owner = self.prefix.clone();
let message = &request.source.message.message_id;
if self
.store
.message_cancelled(&session, message)
.await
.map_err(reason)?
{
saved.goal.settled(
InvocationOutcome::Cancelled {
source: "goal_message_cancelled".into(),
},
self.goal_meter(&session).await,
);
return self.goals.save(saved).await;
}
let accepted = self
.store
.message_admission(&session, message)
.await
.map_err(reason)?
.is_some();
if matches!(
saved.goal.status,
Status::Cancelled | Status::CancellationUnknown
) {
if accepted {
// Its immutable queued input is rejected by the normal Agent
// preparation check, even after a restart. No sibling is stopped.
self.admit_notification(&request).await?;
} else {
// This same gate excludes all Goal dispatches; the SQL read
// observed no accepted original request after prior writes settled.
saved.goal.pending = None;
saved.goal.status = Status::Cancelled;
saved.goal.note = (match saved.goal.arm.language {
Language::En => "Cancelled before execution admission",
Language::ZhCn => "执行接纳前已取消",
Language::ZhTw => "執行接納前已取消",
})
.into();
self.goals.save(saved).await?;
}
return Ok(());
}
if !accepted && saved.goal.status != Status::Active {
return Ok(());
}
if !accepted {
if let Err(reason) = self.validate_background(&saved.goal.arm.grant).await {
saved.goal.status = Status::Blocked;
saved.goal.authority_blocked = true;
saved.goal.note = reason;
// No accepted execution exists and this gate excludes dispatch.
saved.goal.pending = None;
return self.goals.save(saved).await;
}
if !saved.goal.pending.as_ref().expect("pending").dispatched {
saved.goal.pending.as_mut().expect("pending").dispatched = true;
self.goals.save(saved.clone()).await?;
}
}
self.admit_notification(&request).await?;
return Ok(());
}
if saved.goal.status != Status::Active {
return Ok(());
}
if let Err(reason) = self.validate_background(&saved.goal.arm.grant).await {
saved.goal.status = Status::Blocked;
saved.goal.authority_blocked = true;
saved.goal.note = reason;
return self.goals.save(saved).await;
}
if !self.goal_session_idle(&session).await? {
return Ok(());
}
saved.goal.meter(self.goal_meter(&session).await?);
if saved.goal.status == Status::Active {
saved.goal.reserve().map_err(reason)?;
}
self.goals.save(saved).await
}
}