| /* | |
| * 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 crate::{Inner, RunError, RunInput, RunWork, compact, prune, steps}; | |
| use maka_agent_facts::event::CommitError; | |
| use maka_agent_facts::event::EventSink; | |
| 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::input::InvocationInput; | |
| use maka_event_log::StoreError; | |
| use sha2::{Digest, Sha256}; | |
| use std::sync::Arc; | |
| pub(super) struct ModelWork { | |
| pub tools: maka_tools::ToolCatalog, | |
| pub restored: Option<( | |
| maka_agent_facts::handoff::HandoffTools, | |
| maka_agent_facts::handoff::CompactionBudget, | |
| )>, | |
| } | |
| pub async fn run( | |
| inner: Arc<Inner>, | |
| input: RunInput, | |
| cancellation_owner: tokio_util::sync::CancellationToken, | |
| admitted: tokio::sync::oneshot::Sender<()>, | |
| handoff: Option<crate::HandoffGate>, | |
| prepared_claim: Option<crate::continuation::PreparedClaim>, | |
| ) -> Result<Invocation, RunError> { | |
| let cancellation = cancellation_owner.clone(); | |
| if cancellation.is_cancelled() { | |
| return Err(RunError::Cancelled); | |
| } | |
| let manual_message = matches!( | |
| &input.work, | |
| RunWork::Message { | |
| allow_prior_unknown: true, | |
| .. | |
| } | |
| ); | |
| let prior_unknown = tokio::select! { | |
| biased; | |
| _ = cancellation.cancelled() => return Err(RunError::Cancelled), | |
| result = async { | |
| if manual_message { | |
| inner | |
| .log | |
| .check_manual_message_history(&input.invocation.session_id) | |
| .await | |
| } else { | |
| inner | |
| .log | |
| .prepare_prune_candidates(&input.invocation.session_id, None, 0, 0, None) | |
| .await | |
| .map(|_| false) | |
| } | |
| } => result, | |
| }; | |
| let prior_unknown = match prior_unknown { | |
| Ok(value) => value, | |
| Err(StoreError::InvalidTransition(reason)) => { | |
| return Err(RunError::ReconciliationRequired(reason)); | |
| } | |
| Err(StoreError::HistoryUnsettled) => { | |
| return Err(RunError::ReconciliationRequired( | |
| "Session contains unsealed or unresolved prior execution".into(), | |
| )); | |
| } | |
| Err(StoreError::HistoryProviderEffectUnresolved) => { | |
| return Err(RunError::ReconciliationRequired( | |
| "Session contains an unresolved prior provider tool effect".into(), | |
| )); | |
| } | |
| Err(error) => return Err(error.into()), | |
| }; | |
| let claim = match (prepared_claim, &input.work) { | |
| (Some(prepared), _) => { | |
| // The Engine owns Session admission now. Another turn may have | |
| // changed discovery after preflight, but no stale proof may commit. | |
| // Handoff restores its explicit checkpoint instead of live names. | |
| if matches!(input.work, RunWork::Continuation { .. }) | |
| && prepared.loaded_basis != inner.loaded_snapshot(&input.invocation.session_id) | |
| { | |
| return Err(RunError::ContinuationChanged); | |
| } | |
| Some(prepared.claim) | |
| } | |
| ( | |
| None, | |
| RunWork::Continuation { source, tools, .. } | RunWork::Handoff { source, tools, .. }, | |
| ) => Some( | |
| crate::continuation::prepare(&inner, &input, source, tools, &cancellation) | |
| .await? | |
| .claim, | |
| ), | |
| _ => None, | |
| }; | |
| let continuation_base = match &input.work { | |
| RunWork::Handoff { pause, .. } => pause.execution.replay_base, | |
| _ => claim.as_ref().map(|claim| claim.base.high_water), | |
| }; | |
| let (mut input, work) = input.into_context(); | |
| let (opening, model_work) = match work { | |
| RunWork::Handoff { pause, tools, .. } => { | |
| let work = ModelWork { | |
| tools, | |
| restored: Some((pause.execution.tools.clone(), pause.execution.compaction)), | |
| }; | |
| ( | |
| InvocationInput::Handoff { | |
| claim: Box::new(claim.expect("prepared handoff")), | |
| pause, | |
| }, | |
| Some(work), | |
| ) | |
| } | |
| RunWork::Continuation { tools, .. } => ( | |
| InvocationInput::Continuation { | |
| claim: Box::new(claim.expect("prepared continuation")), | |
| request_fingerprint: input | |
| .request_fingerprint | |
| .clone() | |
| .expect("validated continuation fingerprint"), | |
| }, | |
| Some(ModelWork { | |
| tools, | |
| restored: None, | |
| }), | |
| ), | |
| RunWork::Message { | |
| message, | |
| source_messages, | |
| tools, | |
| .. | |
| } => ( | |
| InvocationInput::Message { | |
| content: message, | |
| request_fingerprint: input.request_fingerprint.clone(), | |
| source_messages, | |
| }, | |
| Some(ModelWork { | |
| tools, | |
| restored: None, | |
| }), | |
| ), | |
| RunWork::ContextCompact => ( | |
| InvocationInput::ContextCompact { | |
| request_fingerprint: input | |
| .request_fingerprint | |
| .clone() | |
| .expect("validated compact fingerprint"), | |
| }, | |
| None, | |
| ), | |
| }; | |
| let opening = RuntimeEvent::new( | |
| input.invocation.clone(), | |
| Fact::InvocationOpened { | |
| configuration: Some(Box::new(input.configuration.clone())), | |
| input: opening, | |
| }, | |
| ); | |
| let opening_id = opening.id.clone(); | |
| inner | |
| .log | |
| .clone() | |
| .commit(EventWrite::plain(opening)?) | |
| .await?; | |
| let _ = admitted.send(()); | |
| let result = async { | |
| if !prior_unknown { | |
| prune::run(&inner, &input, &cancellation).await?; | |
| } | |
| if let Some(work) = model_work { | |
| steps::run( | |
| &inner, | |
| &mut input, | |
| work, | |
| &opening_id, | |
| &cancellation, | |
| continuation_base, | |
| handoff.as_ref().expect("model Runs own a handoff gate"), | |
| prior_unknown, | |
| ) | |
| .await | |
| .map(|outcome| (outcome, None)) | |
| } else { | |
| // Compaction owns a larger preparation frame than normal steps. | |
| // Allocate it only for this branch, not for every live invocation. | |
| Box::pin(compact::run( | |
| &inner, | |
| &mut input, | |
| &maka_agent_facts::context::CheckpointMode::Standalone, | |
| &cancellation, | |
| None, | |
| )) | |
| .await | |
| .map(|(outcome, checkpoint)| { | |
| ( | |
| InvocationOutcome::ContextCompactFinished { outcome }, | |
| checkpoint, | |
| ) | |
| }) | |
| } | |
| } | |
| .await; | |
| let result = if matches!(&result, Err(RunError::Commit(CommitError::Retired))) | |
| || (matches!(&result, Ok((outcome, _)) if !matches!(outcome, InvocationOutcome::HandoffPaused { .. })) | |
| && cancellation.is_cancelled()) | |
| { | |
| Err(RunError::Cancelled) | |
| } else { | |
| result | |
| }; | |
| let (outcome, checkpoint) = match &result { | |
| Ok((outcome, checkpoint)) => (outcome.clone(), checkpoint.clone()), | |
| Err(RunError::Cancelled | RunError::Model(maka_model::ModelError::Cancelled)) => ( | |
| InvocationOutcome::Cancelled { | |
| source: "runtime_cancellation".into(), | |
| }, | |
| None, | |
| ), | |
| Err(error) => ( | |
| InvocationOutcome::Failed { | |
| class: failure_class(error).into(), | |
| model_failure: match error { | |
| RunError::Model(error) => error.failure_cause(), | |
| _ => None, | |
| }, | |
| message: Some(error.to_string().chars().take(2048).collect()), | |
| }, | |
| None, | |
| ), | |
| }; | |
| if let Some(checkpoint) = checkpoint { | |
| // A checkpoint is adopted only with its successful terminal in this transaction. | |
| let terminal = | |
| RuntimeEvent::new(input.invocation.clone(), Fact::InvocationEnded { outcome }); | |
| let committed = inner | |
| .log | |
| .append_batch(&[EventWrite::plain(checkpoint)?, EventWrite::plain(terminal)?]) | |
| .await; | |
| if let Err(error) = committed { | |
| if matches!(error, CommitError::Rejected(_)) { | |
| append( | |
| &inner, | |
| &input.invocation, | |
| Fact::InvocationEnded { | |
| outcome: InvocationOutcome::Failed { | |
| class: "event_commit".into(), | |
| model_failure: None, | |
| message: Some(error.to_string().chars().take(2048).collect()), | |
| }, | |
| }, | |
| ) | |
| .await?; | |
| } | |
| return Err(error.into()); | |
| } | |
| inner.clear_loaded(&input.invocation.session_id); | |
| } else { | |
| let pause = match &outcome { | |
| InvocationOutcome::HandoffPaused { pause } => Some(pause.clone()), | |
| _ => None, | |
| }; | |
| append(&inner, &input.invocation, Fact::InvocationEnded { outcome }).await?; | |
| if let Some(pause) = pause { | |
| handoff.as_ref().expect("model Run handoff").sealed(&pause); | |
| } | |
| } | |
| result?; | |
| Ok(input.invocation) | |
| } | |
| fn failure_class(error: &RunError) -> &'static str { | |
| match error { | |
| RunError::Busy => "session_busy", | |
| RunError::ContinuationChanged => "continuation_changed", | |
| RunError::ReconciliationRequired(_) => "reconciliation_required", | |
| RunError::InvalidInput(_) => "invalid_input", | |
| RunError::Cancelled | RunError::Model(maka_model::ModelError::Cancelled) => "cancelled", | |
| RunError::ModelIncomplete => "model_incomplete", | |
| RunError::Commit(_) => "event_commit", | |
| RunError::Store(_) => "event_store", | |
| RunError::Model(maka_model::ModelError::Http { .. }) => "model_http", | |
| RunError::Model(maka_model::ModelError::TimedOut) => "model_timeout", | |
| RunError::Model(maka_model::ModelError::Adapter(_)) => "model_adapter", | |
| RunError::Model(maka_model::ModelError::Provider(failure)) | |
| if failure.reason() | |
| == maka_agent_facts::model::error::ProviderFailureReason::TimedOut => | |
| { | |
| "model_timeout" | |
| } | |
| RunError::Model(maka_model::ModelError::Provider(_)) => "model_provider", | |
| RunError::Model(maka_model::ModelError::ContextOverflow { .. }) => "context_overflow", | |
| RunError::Tool(_) => "tool_execution", | |
| RunError::Internal(_) => "runtime_internal", | |
| RunError::CleanupUnconfirmed => "cleanup_unconfirmed", | |
| } | |
| } | |
| pub(super) async fn append( | |
| inner: &Inner, | |
| invocation: &Invocation, | |
| fact: Fact, | |
| ) -> Result<(), RunError> { | |
| let log = inner.log.clone(); | |
| let event = RuntimeEvent::new(invocation.clone(), fact); | |
| if matches!(event.fact, Fact::InvocationEnded { .. }) { | |
| let now = event | |
| .recorded_at | |
| .duration_since(std::time::UNIX_EPOCH) | |
| .map_err(|error| CommitError::Rejected(error.to_string()))? | |
| .as_millis(); | |
| log.close_run_interactions( | |
| invocation, | |
| maka_agent_facts::interaction::ClosureReason::TurnTerminal, | |
| u64::try_from(now).map_err(|error| CommitError::Rejected(error.to_string()))?, | |
| ) | |
| .await | |
| .map_err(|error| match error { | |
| maka_event_log::StoreError::CommitUnknown(error) => { | |
| CommitError::OutcomeUnknown(error.to_string()) | |
| } | |
| maka_event_log::StoreError::OperationUnknown => { | |
| CommitError::OutcomeUnknown(error.to_string()) | |
| } | |
| other => CommitError::Rejected(other.to_string()), | |
| })?; | |
| } | |
| log.commit(maka_agent_facts::event::EventWrite::plain(event)?) | |
| .await?; | |
| Ok(()) | |
| } | |
| pub(super) fn digest(value: &impl serde::Serialize) -> Result<String, RunError> { | |
| struct Writer(Sha256); | |
| impl std::io::Write for Writer { | |
| fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> { | |
| self.0.update(bytes); | |
| Ok(bytes.len()) | |
| } | |
| fn flush(&mut self) -> std::io::Result<()> { | |
| Ok(()) | |
| } | |
| } | |
| let mut writer = Writer(Sha256::new()); | |
| serde_json::to_writer(&mut writer, value) | |
| .map_err(|error| RunError::Internal(error.to_string()))?; | |
| Ok(format!("sha256:{}", hex::encode(writer.0.finalize()))) | |
| } |