mod tokenizer;
pub use tokenizer::{LEGACY_TOKENIZER, TOKENIZER, canonical_tokenizer, count};
use anda_core::{BoxError, Json};
use serde::{Deserialize, Serialize};
use std::collections::BTreeSet;
pub const MAX_ITEMS: usize = 256;
pub const MAX_ITEM_CONTENT_BYTES: usize = 1024 * 1024;
pub const PACKET_FORMAT: &str = "anda-brain-recall/1";
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[cfg_attr(feature = "mcp", derive(schemars::JsonSchema))]
#[serde(default, deny_unknown_fields)]
pub struct RecallBudget {
#[serde(deserialize_with = "tokenizer::deserialize_tokenizer")]
pub tokenizer: String,
pub max_tokens: u32,
pub context_tokens: u32,
}
impl Default for RecallBudget {
fn default() -> Self {
Self {
tokenizer: TOKENIZER.into(),
max_tokens: 4096,
context_tokens: 49152,
}
}
}
impl RecallBudget {
pub fn validate(&self) -> Result<(), BoxError> {
if canonical_tokenizer(&self.tokenizer).is_none() {
return Err("unsupported Recall tokenizer".into());
}
if !(1..=65_536).contains(&self.max_tokens) {
return Err("Recall max_tokens must be in 1..=65536".into());
}
if !(1..=131_072).contains(&self.context_tokens) {
return Err("Recall context_tokens must be in 1..=131072".into());
}
Ok(())
}
pub fn resolve(
policy: Option<&Self>,
request: Option<&Self>,
) -> Result<Option<Self>, BoxError> {
if let Some(policy) = policy {
policy.validate()?;
}
if let Some(request) = request {
request.validate()?;
}
let (max_tokens, context_tokens) = match (policy, request) {
(None, None) => return Ok(None),
(Some(one), None) | (None, Some(one)) => (one.max_tokens, one.context_tokens),
(Some(policy), Some(request)) => (
policy.max_tokens.min(request.max_tokens),
policy.context_tokens.min(request.context_tokens),
),
};
Ok(Some(Self {
tokenizer: TOKENIZER.into(),
max_tokens,
context_tokens,
}))
}
}
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)]
#[serde(rename_all = "snake_case")]
pub enum Channel {
Primer,
Notes,
Counterparty,
History,
Kip,
Wiki,
Procedures,
}
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)]
#[serde(rename_all = "snake_case")]
pub enum Priority {
Required,
Warning,
VerifiedProcedure,
Relevant,
UnprovenProcedure,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct MemoryItem {
pub id: String,
pub channel: Channel,
pub priority: Priority,
pub content: Json,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
#[serde(default, deny_unknown_fields)]
pub struct Coverage {
pub queried: Vec<Channel>,
pub partial: Vec<Channel>,
pub omitted: Vec<Channel>,
pub unchecked: Vec<Channel>,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct MemoryPacket {
pub format: String,
pub status: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub failed_reason: Option<String>,
pub tokenizer: String,
pub token_limit: u32,
pub items: Vec<MemoryItem>,
pub coverage: Coverage,
pub semantic_complete: bool,
pub action_ready: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SerializedPacket {
pub content: String,
pub tokens: usize,
pub insufficient: bool,
}
pub fn pack(
budget: &RecallBudget,
items: &[MemoryItem],
selected_ids: &[String],
coverage: Coverage,
) -> Result<SerializedPacket, BoxError> {
pack_ranked(
budget,
items,
selected_ids,
coverage,
&std::collections::BTreeMap::new(),
)
}
pub fn pack_ranked(
budget: &RecallBudget,
items: &[MemoryItem],
selected_ids: &[String],
coverage: Coverage,
utility: &std::collections::BTreeMap<String, f64>,
) -> Result<SerializedPacket, BoxError> {
budget.validate()?;
validate_items(items)?;
let mut ordered: Vec<&MemoryItem> = items.iter().collect();
ordered.sort_by(|left, right| {
left.priority
.cmp(&right.priority)
.then_with(|| {
let score = |id: &str| {
utility
.get(id)
.copied()
.filter(|v| v.is_finite() && (0.0..=1.0).contains(v))
};
score(&right.id)
.partial_cmp(&score(&left.id))
.unwrap_or(std::cmp::Ordering::Equal)
})
.then_with(|| left.id.cmp(&right.id))
});
let selected: BTreeSet<&str> = selected_ids.iter().map(String::as_str).collect();
let mut kept: Vec<&MemoryItem> = ordered
.iter()
.filter(|item| item.priority <= Priority::Warning)
.copied()
.collect();
let mut serialized = serialize_packet(budget, items, &kept, &coverage, false)?;
if serialized.tokens > budget.max_tokens as usize {
return insufficient_packet(budget, items, &coverage);
}
for item in ordered {
if item.priority <= Priority::Warning || !selected.contains(item.id.as_str()) {
continue;
}
kept.push(item);
let candidate = serialize_packet(budget, items, &kept, &coverage, false)?;
if candidate.tokens <= budget.max_tokens as usize {
serialized = candidate;
} else {
kept.pop();
}
}
Ok(serialized)
}
pub fn insufficient(
budget: &RecallBudget,
coverage: Coverage,
) -> Result<SerializedPacket, BoxError> {
budget.validate()?;
insufficient_packet(budget, &[], &coverage)
}
pub(crate) fn with_failure_reason(
limits: &RecallBudget,
packet: SerializedPacket,
reason: &str,
) -> Result<SerializedPacket, BoxError> {
debug_assert!(packet.insufficient);
let Some(mut value) = serde_json::from_str::<Option<MemoryPacket>>(&packet.content)? else {
return Ok(packet);
};
value.failed_reason = Some(reason.to_string());
let content = serde_json::to_string(&value)?;
let tokens = count(&content)?;
if tokens > limits.max_tokens as usize {
return Ok(packet);
}
Ok(SerializedPacket {
content,
tokens,
insufficient: true,
})
}
pub(crate) fn longest_fitting<T, E>(
required: usize,
len: usize,
mut fit: impl FnMut(usize) -> Result<Option<T>, E>,
) -> Result<Option<(usize, T)>, E> {
if let Some(all) = fit(len)? {
return Ok(Some((len, all)));
}
let Some(mut best) = fit(required)? else {
return Ok(None);
};
let (mut fits, mut overflows) = (required, len);
while overflows - fits > 1 {
let middle = fits + (overflows - fits) / 2;
match fit(middle)? {
Some(built) => {
fits = middle;
best = built;
}
None => overflows = middle,
}
}
Ok(Some((fits, best)))
}
fn validate_items(items: &[MemoryItem]) -> Result<(), BoxError> {
if items.len() > MAX_ITEMS {
return Err("Recall memory item count exceeds 256".into());
}
let mut ids = BTreeSet::new();
for item in items {
if item.id.is_empty() || !ids.insert(&item.id) {
return Err("Recall memory item ids must be nonempty and unique".into());
}
if serde_json::to_vec(&item.content)?.len() > MAX_ITEM_CONTENT_BYTES {
return Err("Recall memory item content exceeds 1 MiB of serialized JSON".into());
}
}
Ok(())
}
fn normalized(mut coverage: Coverage) -> Coverage {
for channels in [
&mut coverage.queried,
&mut coverage.partial,
&mut coverage.omitted,
&mut coverage.unchecked,
] {
channels.sort_unstable();
channels.dedup();
}
coverage
}
fn delivery_coverage(items: &[MemoryItem], kept: &[&MemoryItem], original: &Coverage) -> Coverage {
let kept_ids: BTreeSet<&str> = kept.iter().map(|item| item.id.as_str()).collect();
let kept_channels: BTreeSet<Channel> = kept.iter().map(|item| item.channel).collect();
let mut coverage = original.clone();
for item in items {
if !kept_ids.contains(item.id.as_str()) {
coverage.omitted.push(item.channel);
if kept_channels.contains(&item.channel) {
coverage.partial.push(item.channel);
}
}
}
for channel in &coverage.omitted {
if kept_channels.contains(channel) {
coverage.partial.push(*channel);
}
}
normalized(coverage)
}
fn serialize_packet(
budget: &RecallBudget,
items: &[MemoryItem],
kept: &[&MemoryItem],
coverage: &Coverage,
insufficient: bool,
) -> Result<SerializedPacket, BoxError> {
#[derive(Serialize)]
struct PacketView<'a> {
format: &'static str,
status: &'static str,
tokenizer: &'static str,
token_limit: u32,
items: &'a [&'a MemoryItem],
coverage: Coverage,
semantic_complete: bool,
action_ready: bool,
}
let packet = PacketView {
format: PACKET_FORMAT,
status: if insufficient {
"budget_insufficient"
} else {
"bounded"
},
tokenizer: TOKENIZER,
token_limit: budget.max_tokens,
items: kept,
coverage: delivery_coverage(items, kept, coverage),
semantic_complete: false,
action_ready: false,
};
let content = serde_json::to_string(&packet)?;
let tokens = count(&content)?;
Ok(SerializedPacket {
content,
tokens,
insufficient,
})
}
fn insufficient_packet(
budget: &RecallBudget,
items: &[MemoryItem],
coverage: &Coverage,
) -> Result<SerializedPacket, BoxError> {
let packet = serialize_packet(budget, items, &[], coverage, true)?;
if packet.tokens <= budget.max_tokens as usize {
return Ok(packet);
}
let content = "null".to_string();
let tokens = count(&content)?;
if tokens > budget.max_tokens as usize {
return Err("Recall budget cannot encode the null failure sentinel".into());
}
Ok(SerializedPacket {
content,
tokens,
insufficient: true,
})
}
#[cfg(test)]
mod tests;