blob: c64a611379e39a5cf36af4fb2a9e78ecfba00efe [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 auxiliary::{failed, persistence};
use futures_util::FutureExt;
use maka_agent_facts::{model::ModelGeneration, tools::ToolError};
use maka_agent_ports::{llm, preferences, session::history};
use maka_capabilities::call::Scope;
use maka_model::{ModelRequest, prompt::Message};
use maka_plugin_core::operation::RecoveryRef;
use maka_session_recap::recap::{self, Recaps};
struct Backend {
owner: Arc<Owner>,
session: String,
generation: Option<(RecoveryRef, ModelSelection)>,
original_resources: Arc<maka_capabilities::call::Resources>,
}
impl Owner {
async fn check_recap(
&self,
scope: &Scope,
session: &str,
) -> Result<RuntimePolicySnapshot, ToolError> {
if !self.settings.recaps || self.stop.is_cancelled() || scope.cancellation.is_cancelled() {
return Err(failed("Session recap unavailable"));
}
self.session(session).await.map_err(failed)?;
let snapshot = self
.configuration
.runtime_policy()
.await
.map_err(persistence)?;
if snapshot.policy.privacy.incognito_active {
return Err(failed("Session recap is unavailable in incognito mode"));
}
Ok(snapshot)
}
fn recaps(
self: &Arc<Self>,
scope: &Scope,
session: String,
generation: Option<(RecoveryRef, ModelSelection)>,
) -> Recaps {
let backend = Arc::new(Backend {
owner: self.clone(),
session,
generation,
original_resources: scope.resources.clone(),
});
Recaps {
store: self.recap_storage.clone(),
history: backend.clone(),
models: backend.clone(),
preferences: backend,
}
}
pub async fn recap(
self: &Arc<Self>,
input: RecapRead,
scope: &Scope,
) -> Result<Option<RecapReceipt>, String> {
self.check_recap(scope, &input.session_id)
.await
.map_err(|e| e.to_string())?;
let recaps = self.recaps(scope, input.session_id.clone(), None);
match input.operation_id {
Some(operation) => recaps.query(scope, &input.session_id, operation).await,
None => recaps.read(scope, &input.session_id).await,
}
.map_err(|e| e.to_string())
}
pub async fn generate_recap(
self: &Arc<Self>,
input: RecapGenerate,
scope: &Scope,
operation: &RecoveryRef,
) -> Result<RecapReceipt, String> {
let operation = operation.clone();
self.resource_work(scope, move |owner, scope| {
Box::pin(async move { owner.generate_recap_inner(input, &scope, &operation).await })
})
.await
}
async fn generate_recap_inner(
self: &Arc<Self>,
input: RecapGenerate,
scope: &Scope,
operation: &RecoveryRef,
) -> Result<RecapReceipt, String> {
let snapshot = self
.check_recap(scope, &input.session_id)
.await
.map_err(|e| e.to_string())?;
let language = self.resolve_language(&snapshot);
// The full admitted identity, including namespace and immutable input, names this cache entry.
let hash = Sha256::digest(serde_json::to_vec(operation).map_err(|e| e.to_string())?);
let id = Uuid::from_bytes(hash[..16].try_into().expect("SHA-256 prefix"));
let recaps = self.recaps(
scope,
input.session_id.clone(),
Some((operation.clone(), input.model)),
);
recaps
.generate(scope, &input.session_id, id, language)
.await
.map_err(|e| e.to_string())
}
}
impl recap::History for Backend {
fn read(
&self,
scope: Scope,
input: history::Read,
) -> BoxFuture<'_, Result<history::Page, ToolError>> {
Box::pin(async move {
input.validate().map_err(failed)?;
if input.session_id != self.session {
return Err(failed("Recap session mismatch"));
}
self.owner.check_recap(&scope, &self.session).await?;
let high_water = *self.owner.store.subscribe_commits().borrow();
let through = input.through.unwrap_or(high_water);
if through > high_water {
return Err(failed("Recap history fence invalid"));
}
let page = self
.owner
.store
.history_text(
&self.session,
through,
input.position,
false,
8192,
8,
96 * 1024,
)
.await
.map_err(persistence)?;
self.owner.check_recap(&scope, &self.session).await?;
Ok(page)
})
}
}
impl preferences::Preferences for Backend {
fn read(&self) -> BoxFuture<'_, Result<preferences::Snapshot, maka_capabilities::Error>> {
Box::pin(async {
let snapshot = self
.owner
.configuration
.runtime_policy()
.await
.map_err(|e| maka_capabilities::Error::Invalid(e.to_string()))?;
Ok(preferences::Snapshot {
revision: snapshot.revision,
privacy: snapshot.policy.privacy,
personalization: snapshot.policy.personalization,
workspace_instructions: snapshot.policy.workspace_instructions.enabled,
})
})
}
}
impl recap::Models for Backend {
fn generate(
&self,
scope: Scope,
input: llm::Generate,
) -> BoxFuture<'_, Result<ModelGeneration, ToolError>> {
Box::pin(async move {
let (reference, model) = self
.generation
.clone()
.ok_or_else(|| failed("Recap generation not admitted"))?;
let owner = self.owner.clone();
let runs = owner.runs.lock().await;
if runs.closing {
return Err(failed("Agent owner closing"));
}
self.owner.check_recap(&scope, &self.session).await?;
let mut call_ticket = scope.resources.reserve().map_err(failed)?;
// Recaps owns a child Scope. Propagate physical cleanup to the
// original Host mutation as well as that child and native retirement.
let mut original_ticket = self.original_resources.reserve().map_err(failed)?;
let mut owner_ticket = owner.auxiliary.resources.reserve().map_err(failed)?;
let session = self.session.clone();
let captured = owner.clone();
let worker = owner.workers.spawn(async move {
call_ticket.start();
original_ticket.start();
owner_ticket.start();
let result = std::panic::AssertUnwindSafe(
captured.recap_model(scope, session, reference, model, input),
)
.catch_unwind()
.await
.unwrap_or_else(|_| {
Err(ToolError::CleanupUnconfirmed(
"Recap worker disappeared".into(),
))
});
let cleanup = match &result {
Err(ToolError::CleanupUnconfirmed(error)) => Err(error.clone()),
_ => Ok(()),
};
if matches!(
result,
Err(ToolError::Persistence(_) | ToolError::CleanupUnconfirmed(_))
) {
captured.runs.lock().await.closing = true;
captured.stop.cancel();
}
call_ticket.complete(cleanup.clone());
original_ticket.complete(cleanup.clone());
owner_ticket.complete(cleanup);
result
});
drop(runs);
// Timeout drops only this observer; the original worker retains both cleanup tickets.
worker
.await
.map_err(|e| ToolError::CleanupUnconfirmed(e.to_string()))?
})
}
}
impl Owner {
async fn recap_model(
&self,
scope: Scope,
session: String,
reference: RecoveryRef,
model: ModelSelection,
input: llm::Generate,
) -> Result<ModelGeneration, ToolError> {
let cancellation = scope.cancellation.child_token();
let _cancel = cancellation.clone().drop_guard();
let operation = async {
input.validate().map_err(failed)?;
let (source, _) = crate::model::Source::select(
self.configuration.clone(),
self.catalog.clone(),
self.credentials.clone(),
model,
None,
session.clone(),
self.revision.clone(),
)
.await
.map_err(failed)?;
let prepared = source.capture_auxiliary().await.map_err(failed)?;
let quote = self
.configuration
.quote_model(prepared.provider_id, source.accepted.binding.model.clone())
.await
.map_err(persistence)?;
self.check_recap(&scope, &session).await?;
let mut prompt = Vec::new();
if let Some(content) = input.system {
prompt.push(Message::System {
content,
provider_options: None,
});
}
prompt.push(Message::user(input.prompt));
let requested = input.max_output_tokens.unwrap_or(1024).min(1024);
self.auxiliary
.generate(
ModelRequest {
language: input.language,
provider: prepared.provider,
prompt,
tools: Vec::new(),
provider_options: prepared.options,
max_output_tokens: Some(
prepared
.main_output_limit
.map_or(requested, |limit| limit.min(requested)),
),
},
cancellation.clone(),
&self.store,
auxiliary::Admission::Operation {
reference: Box::new(reference),
binding: source.accepted.binding.clone(),
session_id: session,
},
quote,
)
.await
.map_err(ToolError::from)
};
tokio::pin!(operation);
let result = tokio::select! {
biased;
_ = self.stop.cancelled() => { cancellation.cancel(); operation.await },
result = &mut operation => result,
};
match result {
Err(error @ (ToolError::Persistence(_) | ToolError::CleanupUnconfirmed(_))) => {
Err(error)
}
_ if cancellation.is_cancelled() => Err(ToolError::OutcomeUnknown(
"Recap generation cancelled; read its original receipt".into(),
)),
result => result,
}
}
}