blob: 831c2e8961ef820c0775fca910d3610d5f0ce641 [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 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())))
}