blob: bea826da03283ff7d8dd6f110dad094acb9c771c [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::SessionConfiguration;
use crate::contract::{RecallPage, RecallPassage};
use futures_util::future::BoxFuture;
use maka_agent_facts::tool_call::ToolRejection;
use maka_agent_facts::tool_output::DurableToolProjection;
use maka_agent_facts::tool_output::ToolOutput;
use maka_agent_facts::tool_output::ToolSuccess;
use maka_agent_facts::tools::*;
use maka_agent_ports::session::catalog;
use maka_agent_ports::session::history;
use maka_assistant::recall::search::{self, History};
use maka_capabilities::call::Scope;
use maka_event_log::sessions::{CatalogFilter, CatalogScope, SessionPosition};
use serde_json::Value;
use std::sync::Arc;
use tokio_util::sync::CancellationToken;
#[derive(Clone)]
pub(crate) struct Recall {
pub(super) store: Arc<maka_event_log::EventLog>,
pub(super) configuration: Arc<maka_config::ConfigurationStore>,
pub(super) prefix: String,
pub(super) data_source: crate::contract::DataSource,
pub(super) enabled: bool,
pub(super) services: maka_host_runtime::application::NativeServices,
pub(super) stop: CancellationToken,
pub(super) workers: tokio_util::task::TaskTracker,
}
impl Recall {
async fn private(&self) -> Result<bool, ToolError> {
Ok(self
.configuration
.runtime_policy()
.await
.map_err(failed)?
.policy
.privacy
.incognito_active)
}
async fn session(
&self,
id: &str,
) -> Result<maka_event_log::sessions::SessionRecord<SessionConfiguration>, String> {
if !super::owns_session(&self.prefix, id) {
return Err("agent_session_unavailable".into());
}
self.store
.get_session(id)
.await
.map_err(|_| "agent_store_unavailable".to_owned())?
.ok_or("agent_session_unavailable".into())
}
async fn check(&self, call: &Scope) -> Result<(), ToolError> {
if call.cancellation.is_cancelled() || self.stop.is_cancelled() {
return Err(failed("Recall cancelled"));
}
if self.private().await? {
return Err(failed("Recall is unavailable in privacy mode"));
}
if let Some(invocation) = call.identity.agent() {
if !self.enabled {
return Err(failed("Recall model access is disabled"));
}
self.session(&invocation.session_id).await.map_err(failed)?;
let boundary = self
.store
.invocation_boundary(&invocation.session_id, &invocation.invocation_id)
.await
.map_err(failed)?
.ok_or_else(|| failed("Recall invocation unavailable"))?
.0;
if boundary.invocation != *invocation
|| matches!(
boundary.state,
maka_event_log::turns::InvocationState::Ended { .. }
)
{
return Err(failed("Recall invocation closed"));
}
}
Ok(())
}
pub async fn query(
self: &Arc<Self>,
mut query: search::Query,
call: &Scope,
) -> Result<RecallPage, String> {
query.validate()?;
self.check(call).await.map_err(|e| e.to_string())?;
if let Some(id) = &query.session_id {
self.session(id).await?;
}
let (titles, catalog_complete) = search::titles(self.as_ref(), call, &query)
.await
.map_err(|e| e.to_string())?;
let mut title_matches = Vec::new();
for title in titles.into_iter().take(query.limit.unwrap_or(8)) {
title_matches.push(crate::service::summary_for(
&self.data_source,
self.session(&title.id).await?,
));
}
let found = search::search(self.clone(), call, &query)
.await
.map_err(|e| e.to_string())?;
let mut passages = Vec::new();
for passage in found
.passages
.into_iter()
.take(query.limit.unwrap_or(8).saturating_sub(title_matches.len()))
{
let session = self.session(&passage.session_id).await?;
let excerpt = passage
.messages
.iter()
.find(|message| message.is_anchor)
.map(|message| message.text.chars().take(1024).collect())
.unwrap_or_default();
passages.push(RecallPassage {
session: crate::service::summary_for(&self.data_source, session),
anchor_message_id: passage.anchor_message_id,
anchor_position: passage.anchor_position,
excerpt,
matched_terms: passage.matched_terms,
});
}
self.check(call).await.map_err(|e| e.to_string())?;
let page = RecallPage {
titles: title_matches,
passages,
complete: catalog_complete && found.searched_every_session,
gaps: found.gaps,
};
Ok(page)
}
pub fn tools(self: &Arc<Self>) -> Vec<ToolRegistration> {
search::definitions()
.into_iter()
.map(|(name, description, input_schema)| ToolRegistration {
definition: ToolDefinition {
provider: None,
name: name.into(),
description: description.into(),
input_schema,
output_schema: None,
freeform: None,
},
nesting: ToolNesting::Nestable,
semantics: ToolSemantics::Parallel,
handler: ToolHandler::Prepared(self.clone()),
})
.collect()
}
}
impl maka_tools::request::RequestBinding for Recall {
fn bind(
&self,
request: maka_tools::request::BindingRequest,
) -> maka_tools::request::BindingFuture {
let recall = Arc::new(self.clone());
Box::pin(async move {
let unavailable = !recall.enabled || recall.private().await?;
if request.cancellation.is_cancelled() || recall.stop.is_cancelled() {
return Err(failed("Recall cancelled"));
}
Ok(request
.tools
.into_iter()
.map(|name| {
let projection = if unavailable {
maka_tools::request::BoundTool::Unavailable
} else {
maka_tools::request::BoundTool::Prepared(recall.clone())
};
(name, projection)
})
.collect())
})
}
}
impl History for Recall {
fn list(
&self,
call: Scope,
input: catalog::List,
) -> BoxFuture<'_, Result<search::Catalog, ToolError>> {
Box::pin(async move {
input.validate().map_err(failed)?;
self.check(&call).await?;
let page = self
.store
.scoped_sessions::<SessionConfiguration>(
CatalogScope::IdPrefix(self.prefix.clone()),
input.revision.as_deref(),
input
.cursor
.as_deref()
.map_or(SessionPosition::Start, SessionPosition::After),
CatalogFilter {
archived_only: false,
include_archived: true,
pending_only: false,
},
)
.await
.map_err(failed)?;
self.check(&call).await?;
Ok(search::Catalog {
revision: page.revision,
next_cursor: page.next_cursor,
entries: page
.sessions
.into_iter()
.map(|session| search::Session {
id: session.id,
title: session.configuration.title,
last_message_at: session.last_message.map(|message| message.recorded_at),
})
.collect(),
})
})
}
fn read(
&self,
call: Scope,
input: history::Read,
) -> BoxFuture<'_, Result<history::Page, ToolError>> {
Box::pin(async move {
input.validate().map_err(failed)?;
self.check(&call).await?;
self.session(&input.session_id).await.map_err(failed)?;
let high_water = *self.store.subscribe_commits().borrow();
let through = input.through.unwrap_or(high_water);
if through > high_water {
return Err(failed("Recall history fence invalid"));
}
let page = self
.store
.history_text(
&input.session_id,
through,
input.position,
false,
8192,
8,
96 * 1024,
)
.await
.map_err(failed)?;
self.session(&input.session_id).await.map_err(failed)?;
self.check(&call).await?;
Ok(page)
})
}
}
enum Input {
Search(search::Query),
More(search::More),
Material(search::Material),
}
impl ToolPreparer for Recall {
fn names(&self) -> Vec<String> {
search::definitions()
.into_iter()
.map(|(name, _, _)| name.into())
.collect()
}
fn prepare(
&self,
name: String,
input: Value,
context: ToolCallContext,
cancellation: CancellationToken,
) -> PreparationFuture {
let recall = Arc::new(self.clone());
Box::pin(async move {
if cancellation.is_cancelled() {
return Err(ToolRejection::Cancelled);
}
let input = match name.as_str() {
"Recall" => {
let mut value: search::Query =
serde_json::from_value(input).map_err(invalid)?;
value.validate().map_err(invalid)?;
Input::Search(value)
}
"RecallMore" => {
let value: search::More = serde_json::from_value(input).map_err(invalid)?;
value.validate().map_err(invalid)?;
Input::More(value)
}
"RecallMaterial" => {
let value: search::Material = serde_json::from_value(input).map_err(invalid)?;
value.validate().map_err(invalid)?;
Input::Material(value)
}
_ => return Err(ToolRejection::Unavailable),
};
Ok(PreparedEffect::new(move |cancellation| {
Box::pin(async move {
// The Engine supplies the admitted ToolCallContext. The
// original native activation owns the request and settlement.
let request = recall
.services
.request(
maka_capabilities::call::Identity::Agent {
invocation: context.invocation,
operation_id: Some(context.operation_id),
},
cancellation,
)
.map_err(failed)?;
let call = request.scope();
let result = async {
recall.check(call).await?;
if let Input::Material(material) = input {
return recall.material(call.clone(), material).await;
}
let found = match input {
Input::Search(query) => {
search::search(recall.clone(), call, &query).await?
}
Input::More(more) => search::more(recall.clone(), call, &more).await?,
Input::Material(_) => unreachable!(),
};
recall.check(call).await?;
let text = found.render();
Ok(ToolSuccess::projected(
ToolOutput::Json(serde_json::to_value(found).map_err(failed)?),
DurableToolProjection::Text { text },
))
}
.await;
request
.finish()
.await
.map_err(ToolError::CleanupUnconfirmed)?;
result
})
}))
})
}
}
fn failed(error: impl std::fmt::Display) -> ToolError {
ToolError::Failed(error.to_string())
}
fn invalid(error: impl std::fmt::Display) -> ToolRejection {
ToolRejection::InvalidInput {
message: error.to_string(),
}
}
mod material;