blob: 7d7ec25145dea7c30cedf4651c26f8b50071e0fc [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::{
diagnostics::TitleFailureCause,
event::{Fact, InvocationInput},
model::{ModelFinishReason, error::ModelFailureCause},
tools::ToolError,
};
use maka_agent_ports::session::title::Policy;
use maka_event_log::usage::AuxiliarySource;
use maka_model::{ModelRequest, prompt::Message};
use tracing::Instrument;
#[cfg(test)]
pub(super) struct WriteGate {
pub(super) entered: tokio::sync::Semaphore,
pub(super) release: tokio::sync::Semaphore,
}
enum GenerationError {
Failure(TitleFailureCause),
Operation(ToolError),
Skipped,
}
impl From<ToolError> for GenerationError {
fn from(error: ToolError) -> Self {
Self::Operation(error)
}
}
impl From<auxiliary::Error> for GenerationError {
fn from(error: auxiliary::Error) -> Self {
match error {
auxiliary::Error::Model(error) => {
error.failure_cause().map_or(Self::Skipped, |cause| {
Self::Failure(TitleFailureCause::Model { cause })
})
}
auxiliary::Error::Admission(_) => Self::Skipped,
auxiliary::Error::Tool(ToolError::Failed(_)) => {
Self::Failure(TitleFailureCause::InvalidOutput)
}
auxiliary::Error::Tool(error) => Self::Operation(error),
}
}
}
impl Owner {
// Called under the original run admission gate; its TaskTracker owns the work through cleanup.
pub(super) fn title_after_message(
self: &Arc<Self>,
invocation: Invocation,
model: crate::model::AcceptedModel,
) {
if !self.settings.automatic_titles || self.stop.is_cancelled() {
return;
}
let Ok(mut ticket) = self.auxiliary.resources.reserve() else {
return;
};
let owner = self.clone();
let span = tracing::error_span!("agent_title_worker", session_id = %invocation.session_id,
turn_id = %invocation.turn_id, run_id = %invocation.run_id,
invocation_id = %invocation.invocation_id, activation = %self.revision.activation);
self.workers.spawn(
async move {
ticket.start();
let completion =
std::panic::AssertUnwindSafe(owner.generate_title(invocation, model))
.catch_unwind()
.await;
let fatal = match &completion {
Err(_) => Some(("title_worker_panicked", "worker_panicked")),
Ok(Err(ToolError::Persistence(_))) => {
Some(("title_worker_persistence_failed", "persistence_failed"))
}
Ok(Err(ToolError::CleanupUnconfirmed(_))) => {
Some(("title_worker_cleanup_unconfirmed", "cleanup_unconfirmed"))
}
_ => None,
};
let result = completion.unwrap_or_else(|_| {
Err(ToolError::CleanupUnconfirmed(
"Auxiliary title worker disappeared".into(),
))
});
let cleanup = match &result {
Err(ToolError::CleanupUnconfirmed(error)) => Err(error.clone()),
_ => Ok(()),
};
ticket.complete(cleanup);
if let Some((stage, cause)) = fatal {
tracing::error!(
stage,
cause,
consequence = "owner_closing",
"Agent title worker failed"
);
owner.runs.lock().await.closing = true;
owner.stop.cancel();
}
}
.instrument(span),
);
}
pub(super) async fn generate_title(
&self,
invocation: Invocation,
model: crate::model::AcceptedModel,
) -> Result<(), ToolError> {
let Some(session) = self
.store
.get_session::<SessionConfiguration>(&invocation.session_id)
.await
.map_err(persistence)?
else {
return Ok(());
};
if session.configuration.title_is_manual
|| session.archived
|| self
.store
.session_manager(&invocation.session_id)
.await
.map_err(persistence)?
.is_some()
{
return Ok(());
}
let Some(opening) = self
.store
.first_message_opening(&invocation.session_id)
.await
.map_err(|error| match error {
maka_event_log::StoreError::PrefixTooLarge => failed(error),
other => persistence(other),
})?
else {
return Ok(());
};
if opening.event.invocation != invocation {
return Ok(());
}
let Fact::InvocationOpened {
input:
InvocationInput::Message {
content,
source_messages,
..
},
configuration: Some(configuration),
..
} = opening.event.fact
else {
return Ok(());
};
if configuration.model.as_ref() != Some(&model.binding) {
return Ok(());
}
let text = source_messages
.first()
.map_or(content.text.as_str(), |source| {
source.unprepared_content.text.as_str()
});
let snapshot = self
.configuration
.runtime_policy()
.await
.map_err(persistence)?;
let language = self.resolve_language(&snapshot);
let Some(input) = maka_session_title::Title.prepare(text, language) else {
return Ok(());
};
let cancellation = self.stop.child_token();
let _cancel = cancellation.clone().drop_guard();
let operation = async {
input
.validate()
.map_err(|_| GenerationError::Failure(TitleFailureCause::Preparation))?;
let source = crate::model::Source::restore(
self.configuration.clone(),
self.credentials.clone(),
model,
invocation.session_id.clone(),
self.revision.clone(),
)
.map_err(|_| GenerationError::Failure(TitleFailureCause::Preparation))?;
let prepared = source
.capture_auxiliary()
.await
.map_err(|_| GenerationError::Failure(TitleFailureCause::Preparation))?;
let requested = input.max_output_tokens.unwrap_or(2048).min(2048);
let quote = self
.configuration
.quote_model(prepared.provider_id, source.accepted.binding.model.clone())
.await
.map_err(persistence)?;
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));
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::Source(AuxiliarySource::SessionTitle {
invocation: invocation.clone(),
}),
quote,
)
.await
.map_err(GenerationError::from)
};
tokio::pin!(operation);
let (result, timed_out) = tokio::select! {
biased;
_ = tokio::time::sleep(Duration::from_secs(30)) => { cancellation.cancel(); (operation.await, true) },
result = &mut operation => (result, false),
};
// Uncertain persistence or physical cleanup outranks the logical deadline.
let result = match result {
Err(GenerationError::Operation(error)) => return Err(error),
_ if self.stop.is_cancelled() => return Ok(()),
_ if timed_out => Err(TitleFailureCause::Model {
cause: ModelFailureCause::TimedOut,
}),
Err(GenerationError::Skipped) => return Ok(()),
Err(GenerationError::Failure(cause)) => Err(cause),
Ok(generated) if generated.finish_reason == ModelFinishReason::Length => {
Err(TitleFailureCause::OutputLimit)
}
Ok(generated) if generated.finish_reason != ModelFinishReason::Stop => {
Err(TitleFailureCause::InvalidOutput)
}
Ok(generated) => {
maka_session_title::normalize(&generated.text).ok_or(TitleFailureCause::EmptyOutput)
}
};
let title = match result {
Ok(title) => title,
Err(cause) => {
self.store
.record_title_failure(invocation.clone(), cause)
.await
.map_err(persistence)?;
return Ok(());
}
};
// Main execution also moves the public revision. Retry metadata only;
// each attempt gives queued manual changes and shutdown a turn at the gate.
loop {
let runs = self.runs.lock().await;
if runs.closing || self.stop.is_cancelled() {
return Ok(());
}
let Some(current) = self
.store
.get_session::<SessionConfiguration>(&invocation.session_id)
.await
.map_err(persistence)?
else {
return Ok(());
};
if current.archived
|| current.configuration.title_is_manual
|| current.configuration.title != session.configuration.title
|| self
.store
.session_manager(&invocation.session_id)
.await
.map_err(persistence)?
.is_some()
{
return Ok(());
}
#[cfg(test)]
{
let gate = self.title_write_gate.lock().unwrap().take();
if let Some(gate) = gate {
gate.entered.add_permits(1);
gate.release.acquire().await.unwrap().forget();
}
}
let title = title.clone();
match self
.store
.update_session_metadata(
&invocation.session_id,
current.revision,
move |configuration: &mut SessionConfiguration| {
if !configuration.title_is_manual {
configuration.title = title;
}
Ok(())
},
)
.await
{
Ok(maka_event_log::sessions::SessionMutation::Committed(_)) => return Ok(()),
Ok(maka_event_log::sessions::SessionMutation::RevisionConflict { .. }) => continue,
Err(
maka_event_log::StoreError::SessionNotFound
| maka_event_log::StoreError::SessionRetired
| maka_event_log::StoreError::SessionBusy,
) => return Ok(()),
Err(error) => return Err(persistence(error)),
}
}
}
}