blob: e8e8c9ce49273c92a99b501d3f4389c685c98867 [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::search::{History, Session};
use maka_agent_facts::tools::ToolError;
use maka_agent_ports::session::catalog;
use maka_agent_ports::session::history::Chunk;
use maka_agent_ports::session::history::Page;
use maka_agent_ports::session::history::Position;
use maka_agent_ports::session::history::Read;
use maka_capabilities::call::Scope;
use std::{collections::VecDeque, sync::Arc};
pub(super) struct Reader {
history: Arc<dyn History>,
call: Scope,
input: Read,
chunks: VecDeque<Chunk>,
done: bool,
assembling: Option<Chunk>,
cursors: std::collections::BTreeSet<String>,
}
impl Reader {
pub fn new(
history: Arc<dyn History>,
call: Scope,
session: String,
through: Option<u64>,
) -> Self {
Self {
history,
call,
input: Read {
include_thinking: false,
session_id: session,
through,
position: Position::Start,
},
chunks: VecDeque::new(),
done: false,
assembling: None,
cursors: std::collections::BTreeSet::new(),
}
}
pub fn through(&self) -> Option<u64> {
self.input.through
}
pub async fn next(&mut self) -> Result<Option<Chunk>, ToolError> {
loop {
if self.call.cancellation.is_cancelled() {
return Err(failed("Recall cancelled"));
}
if let Some(chunk) = self.chunks.pop_front() {
if chunk
.offset
.checked_add(chunk.text.len() as u64)
.is_none_or(|end| end > chunk.total_bytes)
|| (chunk.text.is_empty() && chunk.offset < chunk.total_bytes)
{
return Err(failed("History chunk did not advance"));
}
if let Some(current) = &mut self.assembling {
if current.position != chunk.position
|| current.text.len() as u64 != chunk.offset
|| current.total_bytes != chunk.total_bytes
{
return Err(failed("History continuation changed"));
}
current.text.push_str(&chunk.text);
} else {
if chunk.offset != 0 {
return Err(failed("History message starts mid-text"));
}
self.assembling = Some(chunk);
}
if self
.assembling
.as_ref()
.is_some_and(|value| value.text.len() as u64 == value.total_bytes)
{
return Ok(self.assembling.take());
}
continue;
}
if self.done {
if self.assembling.is_some() {
return Err(failed("History ended before its message"));
}
return Ok(None);
}
let Page::Ready {
through,
chunks,
after,
..
} = self
.history
.read(self.call.clone(), self.input.clone())
.await
.map_err(failed)?;
if self.input.through.is_some_and(|fence| fence != through) {
return Err(failed("History fence changed"));
}
self.input.through = Some(through);
if let Some(cursor) = after {
let next = Position::After { cursor };
let identity = serde_json::to_string(&next).map_err(failed)?;
if next == self.input.position || !self.cursors.insert(identity) {
return Err(failed("History continuation did not advance"));
}
self.input.position = next;
}
self.done = after.is_none();
self.chunks = chunks.into();
tokio::task::yield_now().await;
}
}
}
pub(super) async fn sessions(
history: &dyn History,
call: &Scope,
selected: Option<&str>,
) -> Result<(Vec<Session>, bool), ToolError> {
let mut input = catalog::List {
include_archived: true,
..Default::default()
};
let mut all = Vec::new();
let mut seen = std::collections::BTreeSet::new();
loop {
if call.cancellation.is_cancelled() {
return Err(failed("Recall cancelled"));
}
let page = history
.list(call.clone(), input.clone())
.await
.map_err(failed)?;
if input
.revision
.as_ref()
.is_some_and(|revision| revision != &page.revision)
{
return Err(failed("Recall catalog revision changed"));
}
for session in page.entries {
if selected.is_none_or(|id| id == session.id) {
all.push(session);
}
}
if page.next_cursor.is_none() {
all.sort_by(|a, b| {
b.last_message_at
.cmp(&a.last_message_at)
.then_with(|| a.id.cmp(&b.id))
});
return Ok((all, true));
}
if !seen.insert(page.next_cursor.clone().expect("continuation exists")) {
return Err(failed("Recall catalog continuation repeated"));
}
input.revision = Some(page.revision);
input.cursor = page.next_cursor;
}
}
pub(super) fn failed(error: impl std::fmt::Display) -> ToolError {
ToolError::Failed(error.to_string())
}