Skip to main content

systemprompt_models/brain/
recall.rs

1//! Read-side values of one recall.
2//!
3//! The recall context, scored facts, the injection plan, the audit of
4//! what was injected or withheld and the reranker's record
5//! ([`RecallRerank`]). The query a recall carries is [`RecallQuery`]
6//! (`recall_query`).
7//!
8//! Copyright (c) systemprompt.io — Business Source License 1.1.
9//! See <https://systemprompt.io> for licensing details.
10
11use std::collections::BTreeMap;
12
13use chrono::{DateTime, Utc};
14use serde::{Deserialize, Serialize};
15use systemprompt_identifiers::{BrainFactId, BrainRecallId, UserId, WorkflowId};
16
17use super::fact::BrainFact;
18use super::scope::ScopeLevel;
19use crate::auth::RequestActor;
20use crate::origin::ClientKind;
21
22pub use super::recall_query::RecallQuery;
23
24/// A retrieval tier, fused by reciprocal rank.
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
26#[serde(rename_all = "snake_case")]
27pub enum RetrievalTier {
28    Pinned,
29    Subject,
30    Linked,
31    Context,
32    FullText,
33    Extension,
34}
35
36/// Why a candidate fact did not reach the model.
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
38#[serde(rename_all = "snake_case")]
39pub enum WithheldReason {
40    Scope,
41    Source,
42    AgentCapped,
43    Stale,
44    ReviewDue,
45    Disputed,
46    Quota,
47    MaxFacts,
48    Budget,
49    LowScore,
50    ContextCap,
51    UnknownName,
52    NoSignal,
53    Unavailable,
54}
55
56impl WithheldReason {
57    #[must_use]
58    pub const fn as_str(self) -> &'static str {
59        match self {
60            Self::Scope => "scope",
61            Self::Source => "source",
62            Self::AgentCapped => "agent_capped",
63            Self::Stale => "stale",
64            Self::ReviewDue => "review_due",
65            Self::Disputed => "disputed",
66            Self::Quota => "quota",
67            Self::MaxFacts => "max_facts",
68            Self::Budget => "budget",
69            Self::LowScore => "low_score",
70            Self::ContextCap => "context_cap",
71            Self::UnknownName => "unknown_name",
72            Self::NoSignal => "no_signal",
73            Self::Unavailable => "unavailable",
74        }
75    }
76}
77
78/// Who is asking, from where, at what time.
79///
80/// `now` is computed once per recall and is the only clock the tiers,
81/// the lifecycle check and the importance score read. `as_of` is the
82/// instant the facts are read at: `now` unless the request asked for an
83/// earlier view, in which case every tier admits only facts observed by
84/// then and valid then. `held_keys` are the working-set scope keys the
85/// principal holds (`org`, `user:<id>`, `project:<key>` and every
86/// audience subject a resolver enumerated), built once per recall.
87#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
88pub struct RecallCtx {
89    pub user_id: UserId,
90    pub actor: RequestActor,
91    pub client: ClientKind,
92    pub project: Option<String>,
93    pub workflow: Option<WorkflowId>,
94    pub scopes: Vec<ScopeLevel>,
95    pub now: DateTime<Utc>,
96    pub as_of: DateTime<Utc>,
97    #[serde(default)]
98    pub held_keys: Vec<String>,
99}
100
101impl RecallCtx {
102    #[must_use]
103    pub fn new(user_id: UserId, actor: RequestActor, now: DateTime<Utc>) -> Self {
104        Self {
105            user_id,
106            actor,
107            client: ClientKind::Other,
108            project: None,
109            workflow: None,
110            scopes: ScopeLevel::ALL.to_vec(),
111            now,
112            as_of: now,
113            held_keys: Vec::new(),
114        }
115    }
116
117    #[must_use]
118    pub fn with_as_of(mut self, as_of: DateTime<Utc>) -> Self {
119        self.as_of = as_of.min(self.now);
120        self
121    }
122
123    #[must_use]
124    pub fn with_held_keys(mut self, keys: Vec<String>) -> Self {
125        self.held_keys = keys;
126        self
127    }
128
129    #[must_use]
130    pub const fn with_client(mut self, client: ClientKind) -> Self {
131        self.client = client;
132        self
133    }
134
135    #[must_use]
136    pub fn with_project(mut self, project: impl Into<String>) -> Self {
137        self.project = Some(project.into());
138        self
139    }
140
141    #[must_use]
142    pub fn with_workflow(mut self, workflow: WorkflowId) -> Self {
143        self.workflow = Some(workflow);
144        self
145    }
146
147    #[must_use]
148    pub fn with_scopes(mut self, scopes: Vec<ScopeLevel>) -> Self {
149        self.scopes = scopes;
150        self
151    }
152}
153
154/// A fact's rank in one tier.
155#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
156pub struct TierRank {
157    pub tier: RetrievalTier,
158    pub rank: u32,
159}
160
161/// A retrieved fact with its fused score and the ranks behind it.
162#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
163pub struct ScoredFact {
164    pub fact: BrainFact,
165    pub score: f64,
166    pub ranks: Vec<TierRank>,
167}
168
169/// A fact that was not injected, and why.
170#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
171pub struct WithheldFact {
172    pub id: BrainFactId,
173    pub reason: WithheldReason,
174}
175
176/// The facts chosen for one request within its token budget.
177#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
178pub struct InjectionPlan {
179    pub facts: Vec<ScoredFact>,
180    pub withheld: Vec<WithheldFact>,
181    pub tokens: u32,
182}
183
184/// A fact at the revision that was injected.
185#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
186pub struct FactRef {
187    pub id: BrainFactId,
188    pub revision: i32,
189}
190
191/// The block a conversation's last block-producing recall stored.
192///
193/// It names the facts at their revisions; same-turn reuse resends it
194/// unchanged. `late` marks a block a recall produced after its gateway
195/// deadline: it was stored, never injected, for the turn's next request.
196#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
197pub struct StoredBlock {
198    pub rendered: String,
199    pub as_of: DateTime<Utc>,
200    pub facts: Vec<FactRef>,
201    #[serde(default)]
202    pub late: bool,
203}
204
205/// The audit of one recall: what was injected, what was withheld by
206/// reason, the tokens spent and the instant the facts were read at.
207#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
208pub struct RecallAudit {
209    pub recall_id: BrainRecallId,
210    pub injected: Vec<FactRef>,
211    pub withheld: BTreeMap<WithheldReason, u32>,
212    pub tokens: u32,
213    pub as_of: DateTime<Utc>,
214}
215
216impl RecallAudit {
217    #[must_use]
218    pub fn withheld_total(&self) -> u32 {
219        self.withheld.values().sum()
220    }
221}
222
223/// The rank a reranker gave one injected fact, 1 being first.
224#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
225pub struct RerankedFact {
226    pub id: BrainFactId,
227    pub rank: u32,
228}
229
230/// Which reranker ordered a recall's visible facts and the rank it gave
231/// each injected fact it returned; an injected fact the reranker omitted
232/// has no entry.
233#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
234pub struct RecallRerank {
235    pub reranker: String,
236    pub ranks: Vec<RerankedFact>,
237}