Skip to main content

systemprompt_models/brain/
recall.rs

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