Skip to main content

brain_db_sdk/http/
types.rs

1//! The Brain HTTP contract — request/response types for [`super::BrainHttpClient`].
2//!
3//! Field names are the JSON wire names (snake_case, Rust's default), identical
4//! across the Rust, Python, and TypeScript SDKs. Optional request fields skip
5//! when `None`.
6
7// The types below mirror the server's wire structs one-for-one: same names,
8// same field names, same order. Their documentation is the protocol itself —
9// `conformance/protocol.json` carries every field's type and serde attributes,
10// and the corpus pins the bytes. A doc comment on each of ~2000 fields would
11// restate the field name and nothing more, and the ones that DO carry meaning
12// beyond their name (written below) would be lost in the noise.
13#![allow(missing_docs)]
14
15use serde::{Deserialize, Serialize};
16
17/// `skip_serializing_if` hands serde a reference whatever the field type, so
18/// the `&bool` is required by the signature serde expects, not a choice.
19#[allow(clippy::trivially_copy_pass_by_ref)]
20fn is_false(b: &bool) -> bool {
21    !*b
22}
23
24// --- encode ----------------------------------------------------------------
25
26/// `encode` input.
27#[derive(Clone, Debug, Default, Serialize)]
28pub struct EncodeInput {
29    pub text: String,
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub session: Option<u64>,
32    #[serde(skip_serializing_if = "Option::is_none")]
33    pub occurred_at: Option<u64>,
34}
35
36/// `encode` result.
37#[derive(Clone, Debug, Deserialize)]
38pub struct EncodeResult {
39    pub memory_id: String,
40    pub was_deduplicated: bool,
41    pub salience: f32,
42    pub kind: u8,
43    pub created_at_unix_nanos: u64,
44    pub auto_edges_added: u32,
45}
46
47// --- recall ----------------------------------------------------------------
48
49/// `recall` input.
50#[derive(Clone, Debug, Default, Serialize)]
51pub struct RecallInput {
52    pub query: String,
53    #[serde(skip_serializing_if = "Option::is_none")]
54    pub max_results: Option<u32>,
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub subject: Option<String>,
57    /// Return Brain's read-pipeline trace as [`RecallResult::trace`] — which
58    /// retrievers ran, how they were fused, what the filter chain dropped,
59    /// what the reranker moved. Several KB per call, so leave it off in hot
60    /// paths and switch it on when chasing a bad answer. Served by the cloud
61    /// gateway; a server without it ignores the field and returns no trace.
62    #[serde(skip_serializing_if = "Option::is_none")]
63    pub include_trace: Option<bool>,
64}
65
66/// One recalled memory.
67#[derive(Clone, Debug, Deserialize)]
68pub struct MemoryHit {
69    pub memory_id: String,
70    pub text: String,
71    pub similarity_score: f32,
72    pub confidence: f32,
73    pub salience: f32,
74    pub kind: u8,
75    pub created_at_unix_nanos: u64,
76}
77
78/// `recall` result.
79#[derive(Clone, Debug, Deserialize)]
80pub struct RecallResult {
81    pub answer_kind: String,
82    pub memories: Vec<MemoryHit>,
83    /// The read-pipeline trace as a JSON **string**, present only when
84    /// `include_trace` was asked for and the server supports it. A string
85    /// rather than a parsed value because its ids are 128-bit integers that a
86    /// JSON number would round; parse it with a big-integer-safe reader.
87    #[serde(default, skip_serializing_if = "Option::is_none")]
88    pub trace: Option<String>,
89}
90
91// --- forget ----------------------------------------------------------------
92
93/// `forget` input.
94#[derive(Clone, Debug, Serialize)]
95pub struct ForgetInput {
96    pub memory_id: String,
97    #[serde(skip_serializing_if = "is_false")]
98    pub hard: bool,
99}
100
101/// `forget` result.
102#[derive(Clone, Debug, Deserialize)]
103pub struct ForgetResult {
104    pub memory_id: String,
105    pub was_already_forgotten: bool,
106    pub edges_removed: u32,
107}
108
109// --- link / unlink ---------------------------------------------------------
110
111/// `link` input.
112#[derive(Clone, Debug, Serialize)]
113pub struct LinkInput {
114    pub source: String,
115    pub target: String,
116    pub kind: String,
117    #[serde(skip_serializing_if = "Option::is_none")]
118    pub weight: Option<f32>,
119}
120
121/// `link` result.
122#[derive(Clone, Debug, Deserialize)]
123pub struct LinkResult {
124    pub source: String,
125    pub target: String,
126    pub kind: String,
127    pub weight: f32,
128    pub created_at_unix_nanos: u64,
129    pub already_existed: bool,
130}
131
132/// `unlink` input.
133#[derive(Clone, Debug, Serialize)]
134pub struct UnlinkInput {
135    pub source: String,
136    pub target: String,
137    pub kind: String,
138}
139
140/// `unlink` result.
141#[derive(Clone, Debug, Deserialize)]
142pub struct UnlinkResult {
143    pub source: String,
144    pub target: String,
145    pub kind: String,
146    pub removed: bool,
147}
148
149// --- plan / reason ---------------------------------------------------------
150
151/// A plan/reason endpoint: `text` or `memory_id`.
152#[derive(Clone, Debug, Default, Serialize)]
153pub struct Endpoint {
154    #[serde(skip_serializing_if = "Option::is_none")]
155    pub text: Option<String>,
156    #[serde(skip_serializing_if = "Option::is_none")]
157    pub memory_id: Option<String>,
158}
159
160impl Endpoint {
161    /// An endpoint from free text.
162    #[must_use]
163    pub fn text(text: impl Into<String>) -> Self {
164        Self {
165            text: Some(text.into()),
166            memory_id: None,
167        }
168    }
169
170    /// An endpoint from an existing memory id.
171    #[must_use]
172    pub fn memory(memory_id: impl Into<String>) -> Self {
173        Self {
174            text: None,
175            memory_id: Some(memory_id.into()),
176        }
177    }
178}
179
180/// `plan` input.
181#[derive(Clone, Debug, Default, Serialize)]
182pub struct PlanInput {
183    pub start: Endpoint,
184    pub goal: Endpoint,
185    #[serde(skip_serializing_if = "Option::is_none")]
186    pub max_steps: Option<u32>,
187    #[serde(skip_serializing_if = "Option::is_none")]
188    pub max_wall_time_ms: Option<u32>,
189    #[serde(skip_serializing_if = "Option::is_none")]
190    pub max_branches: Option<u32>,
191    #[serde(skip_serializing_if = "Option::is_none")]
192    pub strategy: Option<String>,
193}
194
195/// One planned step.
196#[derive(Clone, Debug, Deserialize)]
197pub struct PlanStep {
198    pub step_index: u32,
199    pub memory_id: String,
200    pub text: String,
201    pub transition_kind: String,
202    pub confidence: f32,
203    pub estimated_distance_to_goal: f32,
204}
205
206/// `plan` result.
207#[derive(Clone, Debug, Deserialize)]
208pub struct PlanResult {
209    pub steps: Vec<PlanStep>,
210}
211
212/// `reason` input.
213#[derive(Clone, Debug, Default, Serialize)]
214pub struct ReasonInput {
215    pub observation: Endpoint,
216    #[serde(skip_serializing_if = "Option::is_none")]
217    pub depth: Option<u32>,
218    #[serde(skip_serializing_if = "Option::is_none")]
219    pub confidence_threshold: Option<f32>,
220    #[serde(skip_serializing_if = "Option::is_none")]
221    pub max_inferences: Option<u32>,
222    #[serde(skip_serializing_if = "Option::is_none")]
223    pub budget_wall_time_ms: Option<u32>,
224}
225
226/// One inference step.
227#[derive(Clone, Debug, Deserialize)]
228pub struct InferenceStep {
229    pub step_index: u32,
230    pub claim: String,
231    pub supporting_memories: Vec<String>,
232    pub contradicting_memories: Vec<String>,
233    pub confidence: f32,
234    pub inference_kind: String,
235}
236
237/// `reason` result.
238#[derive(Clone, Debug, Deserialize)]
239pub struct ReasonResult {
240    pub inferences: Vec<InferenceStep>,
241}
242
243// --- identity --------------------------------------------------------------
244
245/// Resolved permission flags.
246#[derive(Clone, Debug, Deserialize)]
247pub struct Permissions {
248    pub can_encode: bool,
249    pub can_recall: bool,
250    pub can_plan: bool,
251    pub can_reason: bool,
252    pub can_forget: bool,
253    pub can_admin: bool,
254}
255
256/// `whoami` result.
257#[derive(Clone, Debug, Deserialize)]
258pub struct Whoami {
259    pub namespace: String,
260    pub space_id: String,
261    pub permissions: Permissions,
262}
263
264/// `capabilities` result.
265#[derive(Clone, Debug, Deserialize)]
266pub struct Capabilities {
267    pub rerank: bool,
268    pub llm_extractor: bool,
269    pub classifier_extractor: bool,
270    pub pattern_extractor: bool,
271    pub schema_namespaces: Vec<String>,
272    pub vector_dim: u16,
273}
274
275// --- memories: list / inspect ----------------------------------------------
276//
277// Every query field below is an `Option` that skips when `None`, including the
278// ones the edge declares as plain `bool`/`String` with a `#[serde(default)]`.
279// An omitted query param is what asks for the edge's default; sending
280// `include_tombstoned=false` and omitting it are the same thing today, but a
281// field whose default is a *named function* (`only_current`,
282// `follow_supersession` — both default to `true`) is not, and a client that
283// cannot send `false` cannot ask those questions at all. One rule for all
284// query fields is cheaper to hold than a per-field one.
285
286/// `memory_list` query — a page of the space's memories.
287#[derive(Clone, Debug, Default, Serialize)]
288pub struct MemoryListQuery {
289    #[serde(skip_serializing_if = "Option::is_none")]
290    pub limit: Option<u32>,
291    #[serde(skip_serializing_if = "Option::is_none")]
292    pub cursor: Option<String>,
293    #[serde(skip_serializing_if = "Option::is_none")]
294    pub dir: Option<String>,
295    #[serde(skip_serializing_if = "Option::is_none")]
296    pub include_tombstoned: Option<bool>,
297}
298
299/// One memory in a `memory_list` page.
300#[derive(Clone, Debug, Deserialize)]
301pub struct MemoryListItem {
302    pub memory_id: String,
303    pub text: String,
304    pub kind: u8,
305    pub state: u8,
306    pub created_at_unix_nanos: u64,
307    pub occurred_at_unix_nanos: u64,
308    pub last_accessed_at_unix_nanos: u64,
309    pub salience: f32,
310    pub access_count: u32,
311    pub statement_count: u32,
312    pub entity_count: u32,
313    pub relation_count: u32,
314}
315
316/// `memory_list` result. `next_cursor` is absent on the last page.
317#[derive(Clone, Debug, Deserialize)]
318pub struct MemoryListPage {
319    pub items: Vec<MemoryListItem>,
320    pub next_cursor: Option<String>,
321}
322
323/// The durable metadata row an encode wrote.
324#[derive(Clone, Debug, Deserialize)]
325pub struct StageRecord {
326    pub memory_id: String,
327    pub kind: u8,
328    pub salience: f32,
329    pub created_at_unix_nanos: u64,
330    pub occurred_at_unix_nanos: u64,
331    pub vector_dim: u32,
332    pub text_len: u32,
333    pub lsn: u64,
334}
335
336/// One text-index field and the analyzed terms the write produced for it.
337#[derive(Clone, Debug, Deserialize)]
338pub struct StageKeywordField {
339    pub field: String,
340    pub terms: Vec<String>,
341}
342
343/// A node in the knowledge graph an encode produced.
344#[derive(Clone, Debug, Deserialize)]
345pub struct StageGraphNode {
346    pub id: String,
347    pub name: String,
348    pub kind: String,
349    pub type_qname: String,
350}
351
352/// An edge in the knowledge graph an encode produced.
353#[derive(Clone, Debug, Deserialize)]
354pub struct StageGraphEdge {
355    pub source: String,
356    pub target: String,
357    pub predicate: String,
358    pub kind: String,
359    pub confidence: f32,
360    pub event_at_unix_nanos: Option<u64>,
361}
362
363/// The knowledge graph an encode produced.
364#[derive(Clone, Debug, Deserialize)]
365pub struct StageGraph {
366    pub nodes: Vec<StageGraphNode>,
367    pub edges: Vec<StageGraphEdge>,
368}
369
370/// Everything the encode pipeline produced for one memory.
371#[derive(Clone, Debug, Deserialize)]
372pub struct StageArtifact {
373    pub vector: Vec<f32>,
374    pub record: Option<StageRecord>,
375    pub hype_questions: Vec<String>,
376    pub keyword_fields: Vec<StageKeywordField>,
377    pub graph: Option<StageGraph>,
378}
379
380/// `memory_inspect` result. `found == false` leaves the rest empty.
381#[derive(Clone, Debug, Deserialize)]
382pub struct MemoryInspect {
383    pub found: bool,
384    pub memory_id: String,
385    pub text: String,
386    pub artifact: StageArtifact,
387}
388
389// --- entities ---------------------------------------------------------------
390
391/// `list_entities` query. Each field is a filter; omitting it applies none.
392#[derive(Clone, Debug, Default, Serialize)]
393pub struct ListEntitiesQuery {
394    #[serde(skip_serializing_if = "Option::is_none")]
395    pub type_id: Option<u32>,
396    #[serde(skip_serializing_if = "Option::is_none")]
397    pub prefix: Option<String>,
398    #[serde(skip_serializing_if = "Option::is_none")]
399    pub mention_count_min: Option<u32>,
400    #[serde(skip_serializing_if = "Option::is_none")]
401    pub include_tombstoned: Option<bool>,
402    #[serde(skip_serializing_if = "Option::is_none")]
403    pub include_merged: Option<bool>,
404    #[serde(skip_serializing_if = "Option::is_none")]
405    pub limit: Option<u32>,
406    /// Resume after the previous page (its `next_cursor`). Keep the other
407    /// filters identical across pages — the cursor is bound to them.
408    #[serde(skip_serializing_if = "Option::is_none")]
409    pub cursor: Option<String>,
410}
411
412/// One entity, as the edge projects it.
413#[derive(Clone, Debug, Deserialize)]
414pub struct EntityDetail {
415    pub entity_id: String,
416    pub entity_type_id: u32,
417    pub canonical_name: String,
418    pub aliases: Vec<String>,
419    pub mention_count: u32,
420    pub created_at_unix_nanos: u64,
421    pub updated_at_unix_nanos: u64,
422    /// The surviving entity when this one was merged away; `None` otherwise.
423    pub merged_into: Option<String>,
424}
425
426/// `list_entities` result.
427#[derive(Clone, Debug, Deserialize)]
428pub struct ListEntitiesResult {
429    pub entities: Vec<EntityDetail>,
430    pub count: usize,
431    /// Present when more rows follow; pass it back as `cursor`. Served by the
432    /// cloud gateway's keyset pagination; a server without it omits the field
433    /// and the page is the whole result.
434    #[serde(default, skip_serializing_if = "Option::is_none")]
435    pub next_cursor: Option<String>,
436}
437
438/// `create_entity` input. `aliases` is omitted when empty.
439#[derive(Clone, Debug, Default, Serialize)]
440pub struct CreateEntityInput {
441    pub entity_type_id: u32,
442    pub canonical_name: String,
443    #[serde(skip_serializing_if = "Vec::is_empty")]
444    pub aliases: Vec<String>,
445}
446
447/// `create_entity` result.
448#[derive(Clone, Debug, Deserialize)]
449pub struct CreateEntityResult {
450    pub entity_id: String,
451}
452
453/// `resolve_entity` input: a name to match against the typed graph.
454#[derive(Clone, Debug, Default, Serialize)]
455pub struct ResolveEntityInput {
456    pub candidate_name: String,
457    #[serde(skip_serializing_if = "String::is_empty")]
458    pub resolution_context: String,
459    /// Entity type id to prefer. The edge treats an absent hint as no hint.
460    #[serde(skip_serializing_if = "Option::is_none")]
461    pub type_hint: Option<u32>,
462    #[serde(skip_serializing_if = "is_false")]
463    pub allow_create: bool,
464}
465
466/// `resolve_entity` result. `entity_id` is set when `outcome` resolved to one
467/// entity; `candidate_ids` is the ranked list when it was ambiguous.
468#[derive(Clone, Debug, Deserialize)]
469pub struct ResolveEntityResult {
470    pub outcome: String,
471    pub tier: u8,
472    pub confidence: f32,
473    pub entity_id: Option<String>,
474    pub candidate_ids: Vec<String>,
475}
476
477/// `traverse_relations` input: a multi-hop walk from one entity.
478#[derive(Clone, Debug, Default, Serialize)]
479pub struct TraverseInput {
480    #[serde(skip_serializing_if = "String::is_empty")]
481    pub direction: String,
482    #[serde(skip_serializing_if = "Vec::is_empty")]
483    pub relation_types: Vec<String>,
484    #[serde(skip_serializing_if = "Option::is_none")]
485    pub max_depth: Option<u32>,
486    #[serde(skip_serializing_if = "Option::is_none")]
487    pub max_nodes: Option<u32>,
488    #[serde(skip_serializing_if = "is_false")]
489    pub include_superseded: bool,
490}
491
492/// One hop of a traversal.
493#[derive(Clone, Debug, Deserialize)]
494pub struct TraversalStep {
495    pub relation_id: String,
496    pub from: String,
497    pub to: String,
498    pub relation_type: String,
499    pub depth: u32,
500    /// Display name of `from` (absent when it could not be resolved).
501    ///
502    /// The server resolves these so a caller does not have to.
503    /// Without them every consumer re-queries each id just to print a name — which is the second call the field exists to remove.
504    #[serde(default, skip_serializing_if = "Option::is_none")]
505    pub from_name: Option<String>,
506    /// Display name of `to` (absent when it could not be resolved).
507    #[serde(default, skip_serializing_if = "Option::is_none")]
508    pub to_name: Option<String>,
509}
510
511/// One path the traversal found, as an ordered list of steps.
512#[derive(Clone, Debug, Deserialize)]
513pub struct TraversalPath {
514    pub steps: Vec<TraversalStep>,
515}
516
517/// `traverse_relations` result. `truncated` means a bound cut the walk short.
518#[derive(Clone, Debug, Deserialize)]
519pub struct TraverseResult {
520    pub paths: Vec<TraversalPath>,
521    pub total_paths: u32,
522    pub truncated: bool,
523}
524
525// --- relations --------------------------------------------------------------
526
527/// `list_relations` query — the relations incident to one entity.
528#[derive(Clone, Debug, Default, Serialize)]
529pub struct ListRelationsQuery {
530    #[serde(skip_serializing_if = "Option::is_none")]
531    pub direction: Option<String>,
532    /// The relation type to filter on. Sent as `type`, which is the query
533    /// parameter the edge reads (`relation_type` would be silently ignored).
534    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
535    pub relation_type: Option<String>,
536    #[serde(skip_serializing_if = "Option::is_none")]
537    pub include_superseded: Option<bool>,
538    #[serde(skip_serializing_if = "Option::is_none")]
539    pub include_tombstoned: Option<bool>,
540    #[serde(skip_serializing_if = "Option::is_none")]
541    pub limit: Option<u32>,
542    /// Resume after the previous page (its `next_cursor`). Keep the other
543    /// filters identical across pages — the cursor is bound to them.
544    #[serde(skip_serializing_if = "Option::is_none")]
545    pub cursor: Option<String>,
546}
547
548/// One relation, as the edge projects it.
549#[derive(Clone, Debug, Deserialize)]
550pub struct RelationDetail {
551    pub relation_id: String,
552    pub relation_type: String,
553    pub from_entity: String,
554    pub to_entity: String,
555    pub confidence: f32,
556    pub valid_from_unix_nanos: u64,
557    pub valid_to_unix_nanos: u64,
558    pub is_symmetric: bool,
559    pub tombstoned: bool,
560    /// Display name of `from_entity` (absent when it could not be resolved).
561    ///
562    /// The server resolves these so a caller does not have to.
563    /// Without them every consumer re-queries each id just to print a name — which is the second call the field exists to remove.
564    #[serde(default, skip_serializing_if = "Option::is_none")]
565    pub from_name: Option<String>,
566    /// Display name of `to_entity` (absent when it could not be resolved).
567    #[serde(default, skip_serializing_if = "Option::is_none")]
568    pub to_name: Option<String>,
569}
570
571/// `list_relations` result.
572#[derive(Clone, Debug, Deserialize)]
573pub struct ListRelationsResult {
574    pub relations: Vec<RelationDetail>,
575    pub count: usize,
576    /// Present when more rows follow; pass it back as `cursor`. Served by the
577    /// cloud gateway's keyset pagination; a server without it omits the field
578    /// and the page is the whole result.
579    #[serde(default, skip_serializing_if = "Option::is_none")]
580    pub next_cursor: Option<String>,
581}
582
583/// `get_relation` query. `follow_supersession` defaults to `true` on the edge:
584/// leave it `None` to follow the chain, or send `Some(false)` for this exact
585/// row.
586#[derive(Clone, Debug, Default, Serialize)]
587pub struct GetRelationQuery {
588    #[serde(skip_serializing_if = "Option::is_none")]
589    pub follow_supersession: Option<bool>,
590}
591
592// --- statements -------------------------------------------------------------
593
594/// `list_statements` query. `only_current` defaults to `true` on the edge —
595/// send `Some(false)` to see superseded rows.
596#[derive(Clone, Debug, Default, Serialize)]
597pub struct ListStatementsQuery {
598    #[serde(skip_serializing_if = "Option::is_none")]
599    pub subject: Option<String>,
600    #[serde(skip_serializing_if = "Option::is_none")]
601    pub predicate: Option<String>,
602    #[serde(skip_serializing_if = "Option::is_none")]
603    pub kind: Option<String>,
604    #[serde(skip_serializing_if = "Option::is_none")]
605    pub min_confidence: Option<f32>,
606    #[serde(skip_serializing_if = "Option::is_none")]
607    pub only_current: Option<bool>,
608    #[serde(skip_serializing_if = "Option::is_none")]
609    pub include_tombstoned: Option<bool>,
610    #[serde(skip_serializing_if = "Option::is_none")]
611    pub limit: Option<u32>,
612    /// Resume after the previous page (its `next_cursor`). Keep the other
613    /// filters identical across pages — the cursor is bound to them.
614    #[serde(skip_serializing_if = "Option::is_none")]
615    pub cursor: Option<String>,
616}
617
618/// Deserialize an `f64` by way of `serde_json::Number`.
619///
620/// A number inside an internally tagged enum is buffered by serde before the
621/// variant is known, and `serde_json`'s `arbitrary_precision` feature renders a
622/// buffered float as a map — at which point a plain `f64` field fails with
623/// "invalid type: map, expected f64". Whether that feature is on is decided by
624/// whoever else is in the dependency graph (this crate's own test profile turns
625/// it on), so the SDK does not get to assume it is off. `Number` accepts both
626/// renderings, and the float is the only number this enum buffers.
627fn de_f64<'de, D>(deserializer: D) -> Result<f64, D::Error>
628where
629    D: serde::Deserializer<'de>,
630{
631    let number = serde_json::Number::deserialize(deserializer)?;
632    number
633        .as_f64()
634        .ok_or_else(|| serde::de::Error::custom(format!("{number} is not an f64")))
635}
636
637/// A literal on the object side of a statement. Internally tagged by `type`,
638/// with snake_case tag values: `{"type": "text", "value": "…"}`.
639#[derive(Clone, Debug, Deserialize)]
640#[serde(tag = "type", rename_all = "snake_case")]
641pub enum StatementValue {
642    Text {
643        value: String,
644    },
645    Integer {
646        value: i64,
647    },
648    Float {
649        #[serde(deserialize_with = "de_f64")]
650        value: f64,
651    },
652    Bool {
653        value: bool,
654    },
655    UnixNanos {
656        value: u64,
657    },
658    Blob {
659        value: Vec<u8>,
660    },
661}
662
663/// What a statement points at. Internally tagged by `kind`, with snake_case tag
664/// values: `{"kind": "entity", "id": "…"}`, `{"kind": "value", "value": {…}}`.
665#[derive(Clone, Debug, Deserialize)]
666#[serde(tag = "kind", rename_all = "snake_case")]
667pub enum StatementObject {
668    Entity { id: String },
669    Value { value: StatementValue },
670    Memory { id: String },
671    Statement { id: String },
672}
673
674/// One statement, as the edge projects it.
675#[derive(Clone, Debug, Deserialize)]
676pub struct StatementDetail {
677    pub statement_id: String,
678    /// The statement's own kind (`Fact`, `Preference`, `Event`) — not the
679    /// `kind` tag inside [`StatementObject`], which discriminates the object.
680    pub kind: String,
681    pub subject: String,
682    pub predicate: String,
683    pub object: StatementObject,
684    pub confidence: f32,
685    pub event_at_unix_nanos: u64,
686    pub valid_from_unix_nanos: u64,
687    pub valid_to_unix_nanos: u64,
688    pub tombstoned: bool,
689    /// Display name of `subject` (absent when it could not be resolved).
690    ///
691    /// The server resolves these so a caller does not have to.
692    /// Without them every consumer re-queries each id just to print a name — which is the second call the field exists to remove.
693    #[serde(default, skip_serializing_if = "Option::is_none")]
694    pub subject_name: Option<String>,
695    /// Display name of an entity object (absent for values / unresolved).
696    #[serde(default, skip_serializing_if = "Option::is_none")]
697    pub object_name: Option<String>,
698}
699
700/// `list_statements` result.
701#[derive(Clone, Debug, Deserialize)]
702pub struct ListStatementsResult {
703    pub statements: Vec<StatementDetail>,
704    pub count: usize,
705    /// Present when more rows follow; pass it back as `cursor`. Served by the
706    /// cloud gateway's keyset pagination; a server without it omits the field
707    /// and the page is the whole result.
708    #[serde(default, skip_serializing_if = "Option::is_none")]
709    pub next_cursor: Option<String>,
710}
711
712/// `get_statement` query. Same defaulting as [`GetRelationQuery`]: absent means
713/// the edge's `true`.
714#[derive(Clone, Debug, Default, Serialize)]
715pub struct GetStatementQuery {
716    #[serde(skip_serializing_if = "Option::is_none")]
717    pub follow_supersession: Option<bool>,
718}
719
720// --- graph ------------------------------------------------------------------
721
722/// `graph_fetch` query — one page of the space's typed graph.
723#[derive(Clone, Debug, Default, Serialize)]
724pub struct GraphFetchQuery {
725    #[serde(skip_serializing_if = "Option::is_none")]
726    pub limit: Option<u32>,
727    #[serde(skip_serializing_if = "Option::is_none")]
728    pub cursor: Option<String>,
729    #[serde(skip_serializing_if = "Option::is_none")]
730    pub include_statements: Option<bool>,
731    #[serde(skip_serializing_if = "Option::is_none")]
732    pub include_memories: Option<bool>,
733    #[serde(skip_serializing_if = "Option::is_none")]
734    pub include_memory_edges: Option<bool>,
735    #[serde(skip_serializing_if = "Option::is_none")]
736    pub include_tombstoned: Option<bool>,
737}
738
739/// One node of the typed graph.
740#[derive(Clone, Debug, Deserialize)]
741pub struct GraphNode {
742    pub id: String,
743    pub kind: String,
744    pub label: String,
745    pub type_qname: String,
746}
747
748/// One edge of the typed graph.
749#[derive(Clone, Debug, Deserialize)]
750pub struct GraphEdge {
751    pub from_id: String,
752    pub to_id: String,
753    pub kind: String,
754    pub label: String,
755}
756
757/// `graph_fetch` result. `next_cursor` is absent on the last page.
758#[derive(Clone, Debug, Deserialize)]
759pub struct GraphPage {
760    pub nodes: Vec<GraphNode>,
761    pub edges: Vec<GraphEdge>,
762    pub next_cursor: Option<String>,
763}
764
765// --- schema -----------------------------------------------------------------
766
767/// `get_schema` query. Absent `namespace` means the caller's own; absent
768/// `version` means the active one.
769#[derive(Clone, Debug, Default, Serialize)]
770pub struct SchemaGetQuery {
771    #[serde(skip_serializing_if = "Option::is_none")]
772    pub namespace: Option<String>,
773    #[serde(skip_serializing_if = "Option::is_none")]
774    pub version: Option<u32>,
775}
776
777/// `get_schema` result.
778#[derive(Clone, Debug, Deserialize)]
779pub struct Schema {
780    pub namespace: String,
781    pub schema_version: u32,
782    pub schema_document: String,
783    pub uploaded_at_unix_nanos: u64,
784    pub validator_version: u32,
785}
786
787/// One schema validation diagnostic, positioned in the document.
788#[derive(Clone, Debug, Deserialize)]
789pub struct SchemaError {
790    pub code: String,
791    pub message: String,
792    pub line: u32,
793    pub column: u32,
794    pub length: u32,
795    pub severity: u8,
796}
797
798/// `upload_schema` input.
799#[derive(Clone, Debug, Default, Serialize)]
800pub struct SchemaUploadInput {
801    pub schema_document: String,
802    #[serde(skip_serializing_if = "is_false")]
803    pub dry_run: bool,
804    #[serde(skip_serializing_if = "is_false")]
805    pub allow_breaking: bool,
806}
807
808/// `upload_schema` result. A non-empty `validation_errors` means nothing landed.
809#[derive(Clone, Debug, Deserialize)]
810pub struct SchemaUploadResult {
811    pub namespace: String,
812    pub schema_version: u32,
813    pub backward_compatible: bool,
814    pub validation_errors: Vec<SchemaError>,
815}
816
817/// `validate_schema` input.
818#[derive(Clone, Debug, Default, Serialize)]
819pub struct SchemaValidateInput {
820    pub schema_document: String,
821}
822
823/// `validate_schema` result. `would_be_version` is what an upload would produce.
824#[derive(Clone, Debug, Deserialize)]
825pub struct SchemaValidateResult {
826    pub namespace: String,
827    pub would_be_version: u32,
828    pub validation_errors: Vec<SchemaError>,
829}
830
831/// `replace_schema` input.
832///
833/// Deliberately not `Default`: `force_drop_existing` is a confirmation for an
834/// irreversible operation, not a mode switch, and the manifest gives it no
835/// serde default — the edge always reads it off the wire. Deriving `Default`
836/// would let `..Default::default()` fill in the confirmation silently.
837#[derive(Clone, Debug, Serialize)]
838pub struct SchemaReplaceInput {
839    pub schema_document: String,
840    /// Must be `true`; the edge rejects the request otherwise.
841    pub force_drop_existing: bool,
842}
843
844/// `replace_schema` result. `dropped_count` is how many declared rows the swap
845/// removed.
846#[derive(Clone, Debug, Deserialize)]
847pub struct SchemaReplaceResult {
848    pub namespace: String,
849    pub schema_version: u32,
850    pub dropped_count: u32,
851    pub validation_errors: Vec<SchemaError>,
852}
853
854#[cfg(test)]
855mod recall_trace_tests {
856    use super::{RecallInput, RecallResult};
857
858    /// The trace is several KB per call, so the field is opt-in AND absent
859    /// from the body when it is not asked for — a hot path's request is
860    /// byte-identical to what it was before the field existed.
861    #[test]
862    fn include_trace_is_omitted_unless_set() {
863        let body = serde_json::to_string(&RecallInput {
864            query: "x".into(),
865            max_results: Some(5),
866            ..Default::default()
867        })
868        .expect("serialize");
869        assert!(!body.contains("include_trace"), "{body}");
870    }
871
872    #[test]
873    fn include_trace_is_sent_when_set() {
874        let body = serde_json::to_string(&RecallInput {
875            query: "x".into(),
876            include_trace: Some(true),
877            ..Default::default()
878        })
879        .expect("serialize");
880        assert!(body.contains(r#""include_trace":true"#), "{body}");
881    }
882
883    /// Parity with the TypeScript client, which carried `includeTrace` while
884    /// this one did not. The trace arrives as a JSON STRING — its ids are
885    /// 128-bit and a JSON number would round them — so it is handed over
886    /// unparsed.
887    #[test]
888    fn trace_round_trips_as_an_opaque_string() {
889        let r: RecallResult = serde_json::from_str(
890            r#"{"answer_kind":"single","memories":[],"trace":"{\"stages\":[]}"}"#,
891        )
892        .expect("deserialize");
893        assert_eq!(r.trace.as_deref(), Some(r#"{"stages":[]}"#));
894    }
895
896    /// A server without the flag returns no trace at all. That is not an
897    /// error, and it must not fail deserialization.
898    #[test]
899    fn a_missing_trace_is_none_not_an_error() {
900        let r: RecallResult =
901            serde_json::from_str(r#"{"answer_kind":"none","memories":[]}"#).expect("deserialize");
902        assert!(r.trace.is_none());
903    }
904}