Skip to main content

brain_db_sdk/
verbs.rs

1//! Ergonomic request builders for the v1 verbs.
2//!
3//! The wire structs in [`crate::wire::types`] mirror the protocol exactly —
4//! every field present, in wire order. That is what the codec needs, but it is
5//! a poor calling convention: most fields have an obvious default and only a
6//! few matter per call. These builders fill the defaults, mint a `request_id`,
7//! and expose just the knobs a caller tunes. The verbs on [`crate::BrainClient`]
8//! accept either the builder (via `.build()`) or a hand-built wire struct.
9
10use crate::client::new_id;
11use crate::wire::types::{
12    ActAs, EncodeRequest, ForgetMode, ForgetRequest, MemoryKindWire, RecallRequest,
13    RecallScopeWire, WaitMode, WireMemoryId,
14};
15
16/// Builder for an ENCODE request. Stores one text memory; `session` defaults
17/// to 0 and `occurred_at` is unset (the server stamps ingest time).
18#[derive(Clone, Debug)]
19pub struct EncodeBuilder {
20    text: String,
21    session_id: u64,
22    occurred_at_unix_nanos: Option<u64>,
23    act_as: Option<ActAs>,
24    wait: WaitMode,
25    allow_duplicates: bool,
26}
27
28impl EncodeBuilder {
29    /// Start an ENCODE for `text` (session 0, no explicit occurred-at).
30    #[must_use]
31    pub fn new(text: impl Into<String>) -> Self {
32        Self {
33            text: text.into(),
34            session_id: 0,
35            occurred_at_unix_nanos: None,
36            act_as: None,
37            wait: WaitMode::Ack,
38            allow_duplicates: false,
39        }
40    }
41
42    /// Set the write-completion mode. [`WaitMode::Ack`] (the default) returns
43    /// after the durable ack; [`WaitMode::Derived`] blocks until async
44    /// derivation completes and returns a populated `trace` on the
45    /// [`EncodeResponse`].
46    #[must_use]
47    pub fn wait(mut self, wait: WaitMode) -> Self {
48        self.wait = wait;
49        self
50    }
51
52    /// Block until async derivation completes and return the full write trace
53    /// — convenience for `.wait(WaitMode::Derived)`.
54    #[must_use]
55    pub fn wait_derived(mut self) -> Self {
56        self.wait = WaitMode::Derived;
57        self
58    }
59
60    /// Run this encode on behalf of another `(namespace, space_id)` identity.
61    /// Requires the connection principal to hold the `can_act_as` grant; set
62    /// per request, never on the client, so a pooled client can serve many
63    /// effective identities.
64    #[must_use]
65    pub fn act_as(mut self, namespace: impl Into<String>, space_id: impl Into<String>) -> Self {
66        self.act_as = Some(ActAs {
67            namespace: namespace.into(),
68            space_id: space_id.into(),
69            grant: 0,
70        });
71        self
72    }
73
74    /// Set the session (conversation) the memory belongs to.
75    #[must_use]
76    pub fn session(mut self, session_id: u64) -> Self {
77        self.session_id = session_id;
78        self
79    }
80
81    /// Set the event time the memory describes (unix nanos). Unset means the
82    /// server stamps the ingest time.
83    #[must_use]
84    pub fn occurred_at(mut self, occurred_at_unix_nanos: u64) -> Self {
85        self.occurred_at_unix_nanos = Some(occurred_at_unix_nanos);
86        self
87    }
88
89    /// Opt out of content dedup and force a distinct memory. By default Brain
90    /// dedupes byte-identical text on `(space_id, session_id, BLAKE3(text))` and
91    /// returns the existing memory (`was_deduplicated = true`) without writing.
92    /// Call this when the same text is a genuinely distinct observation that
93    /// must coexist (e.g. the same fact re-stated at a different `occurred_at`).
94    #[must_use]
95    pub fn allow_duplicates(mut self, allow: bool) -> Self {
96        self.allow_duplicates = allow;
97        self
98    }
99
100    /// Finish into a wire [`EncodeRequest`], minting a fresh `request_id`.
101    #[must_use]
102    pub fn build(self) -> EncodeRequest {
103        EncodeRequest {
104            text: self.text,
105            session_id: self.session_id,
106            request_id: new_id(),
107            txn_id: None,
108            occurred_at_unix_nanos: self.occurred_at_unix_nanos,
109            act_as: self.act_as,
110            wait: self.wait,
111            allow_duplicates: self.allow_duplicates,
112        }
113    }
114}
115
116/// Builder for a RECALL request. Defaults answer the cue across all of the
117/// agent's own memories with text and edges included; the server decides
118/// whether the answer is a single memory, many memories, or none.
119#[derive(Clone, Debug)]
120pub struct RecallBuilder {
121    cue_text: String,
122    subject_name: String,
123    max_results: u32,
124    confidence_threshold: f32,
125    session_filter: Option<Vec<u64>>,
126    age_bound_unix_nanos: Option<u64>,
127    as_of_record_time_unix_nanos: Option<u64>,
128    kind_filter: Option<Vec<MemoryKindWire>>,
129    salience_floor: f32,
130    include_edges: bool,
131    include_graph: bool,
132    include_text: bool,
133    trace: bool,
134    act_as: Option<ActAs>,
135    scope: RecallScopeWire,
136}
137
138impl RecallBuilder {
139    /// Start a RECALL for `cue_text` (up to 10 results, text + edges, own
140    /// agent only, no subject pin).
141    #[must_use]
142    pub fn new(cue_text: impl Into<String>) -> Self {
143        Self {
144            cue_text: cue_text.into(),
145            subject_name: String::new(),
146            max_results: 10,
147            confidence_threshold: 0.0,
148            session_filter: None,
149            age_bound_unix_nanos: None,
150            as_of_record_time_unix_nanos: None,
151            kind_filter: None,
152            salience_floor: 0.0,
153            include_edges: true,
154            include_graph: false,
155            include_text: true,
156            trace: false,
157            act_as: None,
158            scope: RecallScopeWire::Space,
159        }
160    }
161
162    /// Set the recall scope. [`RecallScopeWire::Space`] (default) searches the
163    /// caller's own space; [`RecallScopeWire::Namespace`] fans out across every
164    /// space in the caller's namespace (never crossing namespaces).
165    #[must_use]
166    pub fn scope(mut self, scope: RecallScopeWire) -> Self {
167        self.scope = scope;
168        self
169    }
170
171    /// Run this recall on behalf of another `(namespace, space_id)` identity.
172    /// Requires the connection principal to hold the `can_act_as` grant; set
173    /// per request, never on the client, so a pooled client can serve many
174    /// effective identities.
175    #[must_use]
176    pub fn act_as(mut self, namespace: impl Into<String>, space_id: impl Into<String>) -> Self {
177        self.act_as = Some(ActAs {
178            namespace: namespace.into(),
179            space_id: space_id.into(),
180            grant: 0,
181        });
182        self
183    }
184
185    /// Pin the recall to a named subject (anchors fact-shaped lookups).
186    #[must_use]
187    pub fn subject(mut self, name: impl Into<String>) -> Self {
188        self.subject_name = name.into();
189        self
190    }
191
192    /// Cap the number of results returned.
193    #[must_use]
194    pub fn max_results(mut self, max_results: u32) -> Self {
195        self.max_results = max_results;
196        self
197    }
198
199    /// Recall as the graph stood at this record time (unix nanos).
200    #[must_use]
201    pub fn as_of(mut self, as_of_record_time_unix_nanos: u64) -> Self {
202        self.as_of_record_time_unix_nanos = Some(as_of_record_time_unix_nanos);
203        self
204    }
205
206    /// Drop results below this confidence (0.0..=1.0).
207    #[must_use]
208    pub fn confidence_threshold(mut self, threshold: f32) -> Self {
209        self.confidence_threshold = threshold;
210        self
211    }
212
213    /// Restrict to these sessions.
214    #[must_use]
215    pub fn sessions(mut self, sessions: Vec<u64>) -> Self {
216        self.session_filter = Some(sessions);
217        self
218    }
219
220    /// Restrict to these memory kinds.
221    #[must_use]
222    pub fn kinds(mut self, kinds: Vec<MemoryKindWire>) -> Self {
223        self.kind_filter = Some(kinds);
224        self
225    }
226
227    /// Drop results below this salience floor.
228    #[must_use]
229    pub fn salience_floor(mut self, floor: f32) -> Self {
230        self.salience_floor = floor;
231        self
232    }
233
234    /// Include the per-result local edge set.
235    #[must_use]
236    pub fn include_edges(mut self, include: bool) -> Self {
237        self.include_edges = include;
238        self
239    }
240
241    /// Include graph enrichment (entities / statements / relations).
242    #[must_use]
243    pub fn include_graph(mut self, include: bool) -> Self {
244        self.include_graph = include;
245        self
246    }
247
248    /// Ask for the per-stage read-pipeline trace on the final frame
249    /// (`RecallResponseFrame::trace`). Off by default; costs nothing when off.
250    #[must_use]
251    pub fn trace(mut self, trace: bool) -> Self {
252        self.trace = trace;
253        self
254    }
255
256    /// Include the stored memory text in each result.
257    #[must_use]
258    pub fn include_text(mut self, include: bool) -> Self {
259        self.include_text = include;
260        self
261    }
262
263    /// Finish into a wire [`RecallRequest`], minting a fresh `request_id`.
264    #[must_use]
265    pub fn build(self) -> RecallRequest {
266        RecallRequest {
267            cue_text: self.cue_text,
268            subject_name: self.subject_name,
269            max_results: self.max_results,
270            confidence_threshold: self.confidence_threshold,
271            session_filter: self.session_filter,
272            age_bound_unix_nanos: self.age_bound_unix_nanos,
273            as_of_record_time_unix_nanos: self.as_of_record_time_unix_nanos,
274            kind_filter: self.kind_filter,
275            salience_floor: self.salience_floor,
276            include_edges: self.include_edges,
277            include_graph: self.include_graph,
278            include_text: self.include_text,
279            request_id: Some(new_id()),
280            txn_id: None,
281            trace: self.trace,
282            act_as: self.act_as,
283            scope: self.scope,
284        }
285    }
286}
287
288/// Builder for a FORGET request. Defaults to a soft forget (tombstone with a
289/// grace period); `.hard()` zeroes immediately.
290#[derive(Clone, Debug)]
291pub struct ForgetBuilder {
292    memory_id: WireMemoryId,
293    mode: ForgetMode,
294    act_as: Option<ActAs>,
295}
296
297impl ForgetBuilder {
298    /// Start a soft FORGET of `memory_id`.
299    #[must_use]
300    pub fn new(memory_id: WireMemoryId) -> Self {
301        Self {
302            memory_id,
303            mode: ForgetMode::Soft,
304            act_as: None,
305        }
306    }
307
308    /// Run this forget on behalf of another `(namespace, space_id)` identity.
309    /// Requires the connection principal to hold the `can_act_as` grant; set
310    /// per request, never on the client, so a pooled client can serve many
311    /// effective identities.
312    #[must_use]
313    pub fn act_as(mut self, namespace: impl Into<String>, space_id: impl Into<String>) -> Self {
314        self.act_as = Some(ActAs {
315            namespace: namespace.into(),
316            space_id: space_id.into(),
317            grant: 0,
318        });
319        self
320    }
321
322    /// Switch to a hard forget (immediate zeroing, no grace period).
323    #[must_use]
324    pub fn hard(mut self) -> Self {
325        self.mode = ForgetMode::Hard;
326        self
327    }
328
329    /// Set the forget mode explicitly.
330    #[must_use]
331    pub fn mode(mut self, mode: ForgetMode) -> Self {
332        self.mode = mode;
333        self
334    }
335
336    /// Finish into a wire [`ForgetRequest`], minting a fresh `request_id`.
337    #[must_use]
338    pub fn build(self) -> ForgetRequest {
339        ForgetRequest {
340            memory_id: self.memory_id,
341            mode: self.mode,
342            request_id: new_id(),
343            txn_id: None,
344            act_as: self.act_as,
345        }
346    }
347}