blob: 412215e3d3740f228a13ff60b1af20a52ae762b7 [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, model_attempt};
use maka_agent_facts::continuation::ContinuationClaim;
use maka_agent_facts::continuation::REPLAY_VERSION;
use maka_agent_facts::continuation::ReplayEvidence;
use maka_agent_facts::continuation::RunBoundary;
use maka_agent_facts::continuation::SessionBase;
use maka_agent_facts::event::Fact;
use maka_agent_facts::input::InvocationInput;
use maka_model::prompt::{AssistantPart, Message};
use std::{collections::BTreeSet, sync::Arc};
use tokio_util::sync::CancellationToken;
pub(super) struct PreparedClaim {
pub claim: ContinuationClaim,
/// Candidate evidence, not a second owner of live Session preferences.
pub loaded_basis: BTreeSet<String>,
}
/// Derive admission evidence from the exact sealed source. The caller supplies
/// only its boundary, never a replay digest or a pre-authorized claim.
pub(super) async fn prepare(
inner: &Arc<Inner>,
input: &RunInput,
source: &RunBoundary,
catalog: &maka_tools::ToolCatalog,
cancellation: &CancellationToken,
) -> Result<PreparedClaim, RunError> {
let loaded_basis = inner.loaded_snapshot(&input.invocation.session_id);
let (base, replay) =
inspect(inner, input, source, catalog, cancellation, &loaded_basis).await?;
let pause = match &input.work {
crate::RunWork::Handoff { pause, .. } => Some(pause),
_ => None,
};
let claim = ContinuationClaim {
id: pause.map_or_else(
|| uuid::Uuid::new_v4().to_string(),
|pause| pause.intent.claim_id.clone(),
),
source: source.clone(),
base,
replay,
};
match pause {
Some(pause) => pause.validate_claim(&claim, &input.invocation),
None => claim.validate(&input.invocation),
}
.map_err(invalid)?;
Ok(PreparedClaim {
claim,
loaded_basis,
})
}
pub(super) async fn inspect(
inner: &Arc<Inner>,
input: &RunInput,
source: &RunBoundary,
catalog: &maka_tools::ToolCatalog,
cancellation: &CancellationToken,
loaded_basis: &BTreeSet<String>,
) -> Result<(SessionBase, ReplayEvidence), RunError> {
if source.invocation.session_id != input.invocation.session_id {
return Err(invalid("continuation belongs to another Session"));
}
let prefix = inner
.log
.run_source(
&source.invocation.session_id,
&source.invocation.run_id,
None,
)
.await?
.ok_or_else(|| invalid("continuation source is missing"))?;
if prefix.boundary != *source
|| !matches!(&prefix.last.event.fact, Fact::InvocationEnded { .. })
{
return Err(invalid(
"continuation source is not its exact sealed boundary",
));
}
let Fact::InvocationOpened {
input: opening,
configuration: Some(configuration),
} = &prefix.opening.event.fact
else {
return Err(invalid("continuation source has no observed configuration"));
};
if input.configuration.workspace_identity.is_none()
|| input.configuration.workspace_identity != configuration.workspace_identity
{
return Err(invalid("continuation workspace identity changed"));
}
if let crate::RunWork::Handoff { pause, .. } = &input.work
&& (configuration.as_ref() != &input.configuration
|| !matches!(
&prefix.last.event.fact,
Fact::InvocationEnded {
outcome: maka_agent_facts::event::InvocationOutcome::HandoffPaused { pause: sealed }
} if sealed == pause.as_ref()
))
{
return Err(invalid("handoff does not match its sealed source"));
}
let base = match opening {
InvocationInput::Message { .. } => {
let base = inner
.log
.context_before_run(&source.invocation, usize::MAX, usize::MAX)
.await?;
SessionBase {
high_water: base.source_evidence.high_water,
digest: base.source_evidence.digest,
}
}
InvocationInput::Continuation { claim, .. } | InvocationInput::Handoff { claim, .. } => {
claim.base.clone()
}
_ => return Err(invalid("source is not a resumable model Run")),
};
let context = inner
.log
.read_lineage_context(source, usize::MAX, usize::MAX)
.await?;
let prompt = model_attempt::prompt(
inner,
input,
&context,
cancellation,
match &input.work {
crate::RunWork::Handoff { pause, .. } => pause.execution.replay_base,
_ => Some(base.high_water),
},
&input.invocation.invocation_id,
false,
)
.await?;
let tools = maka_tools::RunTools::new(
input.configuration.language,
inner.log.clone(),
input.invocation.clone(),
catalog.clone(),
input.configuration.tool_mode,
inner
.cells
.clone()
.with_language(input.configuration.code_language),
loaded_basis.clone().into(),
)
.with_model(input.provider.tool_context());
if let crate::RunWork::Handoff { pause, .. } = &input.work {
tools.restore(&pause.execution.tools)?;
}
let definitions = tools.replay_definitions();
// Resume validates names from the full explicit inventory, including tools
// not yet advertised through lazy discovery.
let mut available: std::collections::HashSet<_> = catalog.names().into_iter().collect();
available.extend(definitions.iter().map(|definition| definition.name.clone()));
// A handoff replays settled facts, including rejected calls to unavailable
// tools. It never executes them again; the registered catalog is checked above.
for message in prompt
.iter()
.filter(|_| !matches!(input.work, crate::RunWork::Handoff { .. }))
{
if let Message::Assistant { content, .. } = message {
for part in content {
if let AssistantPart::ToolCall {
tool_name,
provider_executed,
..
} = part
&& *provider_executed != Some(true)
&& !available.contains(tool_name)
{
return Err(invalid("continuation requires an unavailable tool"));
}
}
}
}
let replay = replay(input, prompt, definitions, cancellation)?;
if let crate::RunWork::Handoff { pause, .. } = &input.work
&& replay != pause.execution.replay
{
return Err(invalid("handoff admission replay changed after sealing"));
}
Ok((base, replay))
}
/// Shared by reversible handoff preparation and independently verified admission.
pub(super) fn replay<W>(
input: &RunInput<W>,
prompt: Vec<Message>,
definitions: Vec<maka_model::ToolDefinition>,
cancellation: &CancellationToken,
) -> Result<ReplayEvidence, RunError> {
if !matches!(
prompt.iter().find(|m| !matches!(m, Message::System { .. })),
Some(Message::User { .. })
) || !matches!(
prompt.last(),
Some(Message::User { .. } | Message::Tool { .. })
) && !matches!(prompt.last(), Some(Message::Assistant { content, .. }) if matches!(content.last(), Some(AssistantPart::ToolResult { .. })))
{
return Err(invalid(
"continuation requires stable user/tool replay boundaries",
));
}
let request = model_attempt::prepare_request(
input,
prompt,
definitions,
input.main_output_limit,
maka_agent_facts::context::ModelPurpose::Main,
)?;
if cancellation.is_cancelled() {
return Err(RunError::Cancelled);
}
Ok(ReplayEvidence {
version: REPLAY_VERSION,
digest: request.input_digest,
route_identity: request.route_identity,
})
}
fn invalid(reason: &str) -> RunError {
RunError::ReconciliationRequired(reason.into())
}