use std::time::Duration;
use chrono::{DateTime, Utc};
use systemprompt_identifiers::{
AiRequestId, BrainRecallId, ClientId, ContextId, GatewayConversationId, SessionId, UserId,
WorkflowId,
};
use systemprompt_models::auth::RequestActor;
use systemprompt_models::origin::ClientKind;
use super::errors::RecallError;
use super::render::RecallBlock;
pub const MAX_SUBJECT_HINTS: usize = 2000;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ReuseHint {
NewTurn,
SameTurn,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RecallPrincipal {
pub user_id: UserId,
pub client: ClientKind,
pub actor: RequestActor,
pub project: Option<String>,
pub client_id: Option<ClientId>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[expect(
clippy::struct_field_names,
reason = "each field is named for the identifier type it carries"
)]
pub struct RecallConversation {
pub ai_request_id: Option<AiRequestId>,
pub gateway_conversation_id: Option<GatewayConversationId>,
pub session_id: Option<SessionId>,
pub context_id: Option<ContextId>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RecallRequest {
pub principal: RecallPrincipal,
pub conversation: RecallConversation,
pub subject_hints: String,
pub budget_tokens: u32,
pub deadline: Duration,
pub reuse: ReuseHint,
}
impl RecallRequest {
#[must_use]
pub const fn new(principal: RecallPrincipal, conversation: RecallConversation) -> Self {
Self {
principal,
conversation,
subject_hints: String::new(),
budget_tokens: 0,
deadline: Duration::ZERO,
reuse: ReuseHint::NewTurn,
}
}
#[must_use]
pub fn with_subject_hints(mut self, hints: &str) -> Self {
self.subject_hints = hints.chars().take(MAX_SUBJECT_HINTS).collect();
self
}
#[must_use]
pub const fn with_budget_tokens(mut self, budget_tokens: u32) -> Self {
self.budget_tokens = budget_tokens;
self
}
#[must_use]
pub const fn with_deadline(mut self, deadline: Duration) -> Self {
self.deadline = deadline;
self
}
#[must_use]
pub const fn with_reuse(mut self, reuse: ReuseHint) -> Self {
self.reuse = reuse;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SkipReason {
Disabled,
ClientNotEnabled,
HeaderOff,
NotNewTurn,
NoFacts,
Deadline,
Unavailable,
}
impl SkipReason {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Disabled => "disabled",
Self::ClientNotEnabled => "client_not_enabled",
Self::HeaderOff => "header_off",
Self::NotNewTurn => "not_new_turn",
Self::NoFacts => "no_facts",
Self::Deadline => "deadline",
Self::Unavailable => "unavailable",
}
}
#[must_use]
pub fn descriptor(self) -> String {
format!("skipped={}", self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RecallOutcome {
Injected(RecallBlock),
Skipped { reason: SkipReason },
}
impl RecallOutcome {
#[must_use]
pub fn descriptor(&self) -> String {
match self {
Self::Injected(block) => block.descriptor(),
Self::Skipped { reason } => reason.descriptor(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RecallReceipt {
pub recall_id: BrainRecallId,
pub ai_request_id: AiRequestId,
pub principal: RecallPrincipal,
pub conversation: RecallConversation,
pub outcome: RecallOutcome,
pub workflow: Option<WorkflowId>,
pub recorded_at: DateTime<Utc>,
}
impl RecallReceipt {
#[must_use]
pub fn new(
request: &RecallRequest,
ai_request_id: AiRequestId,
outcome: RecallOutcome,
) -> Self {
Self {
recall_id: BrainRecallId::generate(),
ai_request_id,
principal: request.principal.clone(),
conversation: request.conversation.clone(),
outcome,
workflow: None,
recorded_at: Utc::now(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RecallObservation {
pub ai_request_id: AiRequestId,
pub assistant_text: String,
}
#[async_trait::async_trait]
pub trait MemoryRecall: Send + Sync {
fn name(&self) -> &'static str;
async fn recall(&self, request: &RecallRequest) -> Result<RecallOutcome, RecallError>;
async fn record(&self, receipt: &RecallReceipt) -> Result<(), RecallError>;
async fn observe(&self, _observation: &RecallObservation) -> Result<(), RecallError> {
Ok(())
}
}