blob: 2e23a35c1c91d96985e32b2cbe1ae1323e77e004 [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::Fact;
use maka_agent_facts::event::InvocationOutcome;
use maka_event_log::turns::{InvocationState, TurnBoundary};
pub(super) fn project((boundary, high_water): (TurnBoundary, u64)) -> Execution {
Execution {
session_id: boundary.invocation.session_id,
turn_id: boundary.invocation.turn_id,
run_id: boundary.invocation.run_id,
invocation_id: boundary.invocation.invocation_id,
high_water,
state: match boundary.state {
InvocationState::Admitted => ExecutionState::Admitted,
InvocationState::Running => ExecutionState::Running,
InvocationState::WaitingForUser => ExecutionState::Waiting,
InvocationState::Ended { outcome, .. } => match outcome {
InvocationOutcome::HandoffPaused { .. } => ExecutionState::Paused,
InvocationOutcome::ContextCompactFinished {
outcome: maka_agent_facts::context::CompactOutcome::Failed { .. },
} => ExecutionState::Failed,
InvocationOutcome::Completed | InvocationOutcome::ContextCompactFinished { .. } => {
ExecutionState::Completed
}
InvocationOutcome::Failed { .. } => ExecutionState::Failed,
InvocationOutcome::Cancelled { .. } => ExecutionState::Cancelled,
},
},
}
}
fn parked(reason: ResumeParkReason) -> ResumeResult {
ResumeResult::Parked { reason }
}
impl Owner {
pub async fn work(&self, input: WorkRead) -> Result<Work, String> {
use maka_event_log::message_resolution::MessageExecution as Stored;
self.session(&input.session_id).await?;
let offset = input.cursor.as_ref().map_or(0, |cursor| cursor.offset);
let origin = match input.target {
WorkTarget::Message { message_id } => {
maka_event_log::observation::WorkOrigin::Message(message_id)
}
WorkTarget::Invocation { invocation_id } => {
maka_event_log::observation::WorkOrigin::Invocation(invocation_id)
}
};
let observed = self
.store
.work_observation(
&input.session_id,
origin,
offset,
input
.cursor
.as_ref()
.map(|cursor| cursor.invocation_id.as_str()),
)
.await
.map_err(|_| "agent_work_unavailable")?;
let (shared, boundary) = match observed.execution {
Stored::Missing => return Ok(Work::Missing),
Stored::Pending => return Ok(Work::Pending),
Stored::Cancelled => return Ok(Work::Cancelled),
Stored::Owned(boundary) => (false, boundary),
Stored::Shared(boundary) => (true, boundary),
};
if input
.cursor
.as_ref()
.is_some_and(|cursor| cursor.invocation_id != boundary.invocation.invocation_id)
{
return Ok(Work::Changed);
}
let answer = observed.answer.map(|answer| {
let length = answer.text.len();
AnswerPage {
text: answer.text,
next: (!answer.complete).then(|| AnswerCursor {
invocation_id: boundary.invocation.invocation_id.clone(),
offset: offset + length as u64,
}),
}
});
Ok(Work::Delivered {
shared,
execution: project((*boundary, observed.high_water)),
answer,
pending_interactions: observed.interactions.len() as u32,
})
}
pub async fn execution(&self, input: ExecutionRead) -> Result<ExecutionResult, String> {
self.session(&input.session_id).await?;
let boundary = self
.store
.invocation_boundary(&input.session_id, &input.invocation_id)
.await
.map_err(|_| "agent_execution_unavailable")?;
Ok(match boundary {
Some(boundary) => ExecutionResult::Found {
execution: project(boundary),
},
None => ExecutionResult::Missing,
})
}
pub async fn resume(
self: &Arc<Self>,
input: Resume,
operation: &maka_plugin_core::operation::RecoveryRef,
) -> Result<ResumeResult, String> {
use ResumeParkReason as Reason;
let mut runs = self.runs.lock().await;
let session = self.session(&input.session_id).await?;
let identity = self.operation_id(operation);
// Engine receipts and application operations use different digest encodings.
let fingerprint = format!(
"sha256:{}",
hex::encode(Sha256::digest(operation.fingerprint.as_bytes()))
);
let invocation = Invocation {
session_id: input.session_id.clone(),
turn_id: format!("t_{identity}"),
run_id: format!("r_{identity}"),
invocation_id: format!("i_{identity}"),
};
// Acceptance wins over today's model availability, workspace and archive
// state. The canonical opening is the receipt; there is no resume ledger.
if let Some((accepted, source, recorded_fingerprint)) = self
.store
.continuation_receipt(&invocation.invocation_id)
.await
.map_err(|_| "agent_execution_unavailable")?
{
if accepted != invocation
|| recorded_fingerprint != fingerprint
|| source.invocation.session_id != input.session_id
|| source.invocation.invocation_id != input.source_invocation_id
|| source.high_water != input.source_high_water
{
return Err("agent_resume_conflict".into());
}
let boundary = self
.store
.invocation_boundary(&input.session_id, &invocation.invocation_id)
.await
.map_err(|_| "agent_execution_unavailable")?
.ok_or("agent_execution_unavailable")?;
return Ok(ResumeResult::Started {
execution: project(boundary),
});
}
if runs.closing || self.stop.is_cancelled() {
return Ok(parked(Reason::Closed));
}
if session.archived {
return Ok(parked(Reason::Archived));
}
if runs.controls.contains_key(&input.session_id)
|| !self
.store
.message_queue(&input.session_id)
.await
.map_err(|_| "agent_queue_unavailable")?
.entries
.is_empty()
{
return Ok(parked(Reason::Busy));
}
let Some((boundary, high_water)) = self
.store
.invocation_boundary(&input.session_id, &input.source_invocation_id)
.await
.map_err(|_| "agent_execution_unavailable")?
else {
return Ok(parked(Reason::SourceMissing));
};
if high_water != input.source_high_water {
return Ok(parked(Reason::SourceChanged));
}
if !matches!(
boundary.state,
InvocationState::Ended {
outcome: InvocationOutcome::Failed { .. } | InvocationOutcome::Cancelled { .. },
..
}
) {
return Ok(parked(Reason::SourceNotResumable));
}
let prefix = match self
.store
.run_source(&input.session_id, &boundary.invocation.run_id, None)
.await
{
Ok(Some(prefix)) => prefix,
Ok(None) => return Ok(parked(Reason::SourceMissing)),
Err(_) => return Err("agent_execution_unavailable".into()),
};
if prefix.boundary.invocation != boundary.invocation
|| prefix.boundary.high_water != input.source_high_water
|| !matches!(&prefix.last.event.fact, Fact::InvocationEnded { .. })
{
return Ok(parked(Reason::SourceChanged));
}
let source_boundary = prefix.boundary;
if self
.store
.continuation_for_source(&source_boundary)
.await
.map_err(|_| "agent_execution_unavailable")?
.is_some()
{
return Ok(parked(Reason::AlreadyContinued));
}
let Some(mut configuration) = self
.store
.invocation_configuration(&boundary.invocation)
.await
.map_err(|_| "agent_execution_unavailable")?
else {
return Ok(parked(Reason::SourceNotResumable));
};
if configuration.model.is_none()
|| configuration.orchestration_mode.as_str() == plan::EXECUTION
{
return Ok(parked(Reason::SourceNotResumable));
}
let workspace = &session.configuration.workspace;
if configuration.cwd != workspace.path
|| configuration.workspace_identity.as_ref() != Some(&workspace.identity)
|| workspace::validate(&self.workspace_roots, workspace, &self.managed_worktrees)
.await
.is_err()
{
return Ok(parked(Reason::WorkspaceChanged));
}
let (mut source, prepared) = crate::model::Source::select(
self.configuration.clone(),
self.catalog.clone(),
self.credentials.clone(),
input.model,
input.thinking_level,
input.session_id.clone(),
self.revision.clone(),
)
.await
.map_err(public_error)?;
// A continuation retains the original tool protocol even when the
// user selects a model whose default differs from the original Run.
source.accepted.tool_mode = configuration.tool_mode;
source.accepted.code_language = configuration.code_language;
source.accepted.editing_tools = configuration.editing_tools;
let graph = match self
.resume_graph(&input.session_id, &configuration.orchestration_mode)
.await
{
Ok(graph) => graph,
Err(_) => return Ok(parked(Reason::SourceNotResumable)),
};
let basis = MessageBasis {
language: configuration.language,
compaction_prompt: configuration.compaction_prompt.clone(),
graph,
collaboration_mode: configuration.collaboration_mode,
background_kind: BackgroundKind::Schedule,
tool_discovery: configuration.tool_discovery,
allowed_tools: session.configuration.tools.clone(),
background: None,
internal_tools: self.internal_tools.clone(),
internal_bindings: self.internal_bindings.clone(),
// Ordinary Agents retain their existing queue compatibility. A
// coordinator continuation cannot silently adopt a newer behavior.
behavior: self
.behavior
.as_ref()
.and(configuration.system_prompt.clone()),
model: Some(source.accepted.clone()),
executor: None,
workspace: workspace.clone(),
fingerprint: fingerprint.clone(),
tools: self.tool_basis.clone(),
};
configuration.model = Some(source.accepted.binding.clone());
configuration.thinking_level = source.accepted.thinking_level;
// Replay the original prepared prompt. Native service tools continue to
// resolve the live Session permission boundary through their actual owner.
let tools = self.selected_tools(
&basis,
configuration.tool_discovery,
configuration.editing_tools,
);
let run = RunInput {
invocation: invocation.clone(),
model_source: Some(Arc::new(source)),
model_revision: Some(prepared.revision),
provider_id: prepared.provider_id,
context: prepared.context,
provider: prepared.provider,
provider_options: prepared.options,
main_output_limit: prepared.main_output_limit,
supports_vision: prepared.supports_vision,
request_fingerprint: Some(fingerprint),
configuration,
work: RunWork::Continuation {
source: source_boundary,
tools,
},
};
let prepared = match self.engine.prepare_continuation(run, &self.stop).await {
Ok(prepared) => prepared,
Err(
RunError::InvalidInput(_)
| RunError::ReconciliationRequired(_)
| RunError::Store(
maka_event_log::StoreError::InvalidTransition(_)
| maka_event_log::StoreError::HistoryUnsettled
| maka_event_log::StoreError::HistoryProviderEffectUnresolved,
),
) => {
return Ok(parked(Reason::SafetyCheckFailed));
}
Err(error) => {
if error.is_cleanup_unconfirmed() {
runs.quarantined.insert(input.session_id.clone());
runs.closing = true;
self.stop.cancel();
}
return Err(public_error(error));
}
};
let running = match self
.engine
.start_continuation(prepared, self.stop.child_token())
.await
{
Ok(running) => running,
Err(RunError::Busy) => return Ok(parked(Reason::Busy)),
Err(
RunError::ContinuationChanged
| RunError::InvalidInput(_)
| RunError::ReconciliationRequired(_),
) => {
return Ok(parked(Reason::SafetyCheckFailed));
}
Err(error) => {
if error.is_cleanup_unconfirmed() {
runs.quarantined.insert(input.session_id.clone());
}
runs.closing = true;
self.stop.cancel();
return Err(public_error(error));
}
};
runs.controls.insert(
input.session_id,
Control {
compacting: false,
invocation: invocation.clone(),
basis,
cancellation: running.cancellation(),
failure: None,
},
);
self.workers.spawn(self.clone().drive(running));
let boundary = self
.store
.invocation_boundary(&invocation.session_id, &invocation.invocation_id)
.await
.map_err(|_| "agent_execution_unavailable")?
.ok_or("agent_execution_unavailable")?;
Ok(ResumeResult::Started {
execution: project(boundary),
})
}
}