erebyx-sdk 0.1.1

Rust SDK for EREBYX — persistent AI memory across every AI you use. Encrypted in transit (TLS 1.3) and at rest with envelope encryption (server-held master KEK at v0.1.1); per-user zero-knowledge encryption in v0.2.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
// SPDX-License-Identifier: MIT OR Apache-2.0
//! Types for the EREBYX SDK.

use std::collections::HashMap;

use serde::{Deserialize, Serialize};

// =========================================================================
// Shared building blocks (mirror core/api/schemas/identity_tool.py)
// =========================================================================

/// The substrate's recommended next tool invocation.
///
/// Every lifecycle response carries this hint so the AI consumer has a
/// clear forward-action signal instead of guessing. Mirrors
/// `core/api/schemas/identity_tool.py::SuggestedNextCall`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SuggestedNextCall {
    #[serde(default)]
    pub tool: String,
    #[serde(default)]
    pub args: HashMap<String, serde_json::Value>,
    #[serde(default)]
    pub reason: String,
}

/// Per-response substrate metadata. Carries the schema version pin,
/// the server-side request id (for support tickets + cross-surface
/// tracing), and server-side processing latency.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolMeta {
    #[serde(default)]
    pub schema_version: String,
    #[serde(default)]
    pub request_id: Option<String>,
    #[serde(default)]
    pub latency_ms: Option<i64>,
}

/// A stored memory.
///
/// **Wire field renames (P0-A fix, 2026-05-27):** the substrate's
/// `RememberMemoryItem` (`core/api/routes/remember.py:231-276`) emits
/// `memory_id` not `id`, and `final_score` not `score`. Without the
/// `#[serde(rename = ...)]` annotations every `memory.search()` call
/// would return `Err(Error::Serialization)` against the real
/// substrate. `#[serde(flatten)] extra` is the forward-compat catch-
/// all so future substrate field additions don't get silently dropped.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MemoryRecord {
    #[serde(rename = "memory_id")]
    pub id: String,
    pub content: String,
    pub category: String,
    #[serde(default)]
    pub title: Option<String>,
    #[serde(default)]
    pub anchors: Vec<String>,
    #[serde(default)]
    pub importance: f64,
    #[serde(default)]
    pub created_at: Option<String>,
    #[serde(default)]
    pub updated_at: Option<String>,
    /// Relevance score from search (substrate emits as `final_score`).
    #[serde(default, rename = "final_score")]
    pub score: Option<f64>,
    /// Forward-compat catch-all. The substrate's `RememberMemoryItem`
    /// emits 7 additional fields (`metadata`, `retrieval_path`,
    /// `reranker_score`, `supersedes_id`, `contradiction_ids`,
    /// `evolved_from_id`, `related_via_edges`). Surfacing them as
    /// typed fields is post-launch work; the flatten lands them as
    /// raw JSON in the interim so callers needing them can still
    /// reach them.
    #[serde(flatten)]
    pub extra: std::collections::HashMap<String, serde_json::Value>,
}

/// Result of a memory save operation.
///
/// **Forward-compat (P0-C fix, 2026-05-27):** the substrate's
/// `StoreMemoryResponse` (`core/api/routes/memory.py:473-560`) emits
/// envelope fields including `action`, `dedup`, `enrichment`,
/// `created_at`, `warnings`, `schema_version`,
/// `consolidation_priority`, `content_hash`. The `#[serde(flatten)]
/// extra` is the catch-all until those are typed individually. The
/// `enrichment` field is the customer-facing rename of the substrate-
/// internal `atomization` envelope — same shape `{queued, queue_id}`,
/// canon-clean key name per Genesis Arche brand canon.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SaveResult {
    pub memory_id: String,
    #[serde(default)]
    pub anchors: Vec<String>,
    #[serde(default)]
    pub status: String,
    /// Substrate lifecycle hints (`X-Erebyx-Hint` header). See
    /// [`crate::Memory`] docs for the canonical hint vocabulary.
    /// Empty when the substrate has nothing to recommend.
    #[serde(default)]
    pub hints: Vec<String>,
    /// Tools the substrate ran automatically on this call
    /// (`X-Erebyx-Auto-Fired` header) — typically
    /// `["restore_identity", "load_context"]` on the first call against
    /// a fresh `(instance_id, session_id)` tuple. Empty thereafter.
    #[serde(default)]
    pub auto_fired: Vec<String>,
    /// Forward-compat catch-all for substrate envelope fields not yet
    /// typed individually.
    #[serde(flatten)]
    pub extra: std::collections::HashMap<String, serde_json::Value>,
}

/// Result of a memory search operation.
///
/// **Forward-compat (P0-B fix, 2026-05-27):** the substrate's
/// `RememberResponse` (`core/api/routes/remember.py:279-329`) emits
/// 9 additional fields including the load-bearing `formatted_text`
/// (LLM-readable rendered output), `star_context`, `abstention`,
/// `voice_markers`, `onboarding_hint`, `truncated`, `degraded`,
/// `degraded_reason`, `warnings`. `#[serde(flatten)] extra` is the
/// catch-all until those are typed individually.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SearchResult {
    pub memories: Vec<MemoryRecord>,
    #[serde(default)]
    pub total_found: usize,
    #[serde(default)]
    pub familiarity: f64,
    /// Substrate lifecycle hints (`X-Erebyx-Hint` header).
    #[serde(default)]
    pub hints: Vec<String>,
    /// Tools the substrate ran automatically on this call
    /// (`X-Erebyx-Auto-Fired` header).
    #[serde(default)]
    pub auto_fired: Vec<String>,
    /// Forward-compat catch-all. Notably carries the substrate's
    /// `formatted_text` field — the pre-rendered LLM-readable output
    /// the substrate generates for direct working-context injection.
    #[serde(flatten)]
    pub extra: std::collections::HashMap<String, serde_json::Value>,
}

/// Builder for save operations.
pub struct SaveBuilder<'a> {
    pub(crate) client: &'a crate::client::Memory,
    pub(crate) content: String,
    pub(crate) category: String,
    pub(crate) title: Option<String>,
    pub(crate) anchors: Option<Vec<String>>,
    pub(crate) importance: Option<f64>,
    pub(crate) memory_type: Option<String>,
}

impl<'a> SaveBuilder<'a> {
    pub fn title(mut self, title: &str) -> Self {
        self.title = Some(title.to_string());
        self
    }

    pub fn anchors(mut self, anchors: Vec<&str>) -> Self {
        self.anchors = Some(anchors.into_iter().map(String::from).collect());
        self
    }

    pub fn importance(mut self, importance: f64) -> Self {
        self.importance = Some(importance);
        self
    }

    pub fn memory_type(mut self, memory_type: &str) -> Self {
        self.memory_type = Some(memory_type.to_string());
        self
    }

    pub async fn send(self) -> Result<SaveResult, crate::Error> {
        self.client.execute_save(self).await
    }
}

/// Builder for search operations.
pub struct SearchBuilder<'a> {
    pub(crate) client: &'a crate::client::Memory,
    pub(crate) query: String,
    pub(crate) limit: Option<u32>,
    pub(crate) hint_anchors: Option<Vec<String>>,
    pub(crate) time_range: Option<String>,
    pub(crate) types: Option<Vec<String>>,
}

impl<'a> SearchBuilder<'a> {
    pub fn limit(mut self, limit: u32) -> Self {
        self.limit = Some(limit);
        self
    }

    pub fn hint_anchors(mut self, anchors: Vec<&str>) -> Self {
        self.hint_anchors = Some(anchors.into_iter().map(String::from).collect());
        self
    }

    pub fn time_range(mut self, range: &str) -> Self {
        self.time_range = Some(range.to_string());
        self
    }

    // SIMPLIFY-MODES (Genesis Arche T-5 days, 2026-05-27): the
    // ``.types()`` builder was dropped to match the substrate's MCP
    // launch surface narrowing (PR #963). The builder advertised three
    // values (memory / skill / specialization) but skill + specialization
    // are gated v0.2 features that return empty at v0.1 — 67% of the
    // surface was no-op. The underlying ``self.types`` field on the
    // request struct is retained as ``pub(crate)`` so substrate-internal
    // HTTP routing continues to work. Re-exposed in v0.2 when skill +
    // specialization actually return data.

    pub async fn send(self) -> Result<SearchResult, crate::Error> {
        self.client.execute_search(self).await
    }
}

// =========================================================================
// Wrap-up types
// =========================================================================

/// Result of a session wrap-up operation. Matches the substrate's
/// `WrapUpResponse` shape one-to-one.
///
/// **Schema change (2026-05-27, Bucket B):** prior versions of this
/// struct had `status` / `created_memories` / `errors` / `message`
/// fields that the substrate has never emitted on the canonical
/// `POST /v0/session/wrap-up` route — the substrate returns the full
/// closure receipt (handoff_id + session_id + saves_persisted +
/// reflections_persisted + narrative + next_action_hint + ...).
/// The old field set was silently empty on every successful wrap_up.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WrapUpResult {
    /// The handoff row the AI just persisted.
    #[serde(default)]
    pub handoff_id: String,
    /// Session this handoff closes (echoed from the request body when
    /// supplied; synthesized server-side otherwise).
    #[serde(default)]
    pub session_id: String,
    /// Domain anchors carried on the handoff.
    #[serde(default)]
    pub anchors: Vec<String>,
    /// Plaintext handoff summary the AI wrote.
    #[serde(default)]
    pub summary: String,
    /// Count of memories the wrap_up batch wrote.
    #[serde(default)]
    pub saves_persisted: i64,
    /// Count of reflection entries the wrap_up batch wrote.
    #[serde(default)]
    pub reflections_persisted: i64,
    /// One-line hint the next AI session can use to pick up.
    #[serde(default)]
    pub next_action_hint: String,
    /// Pre-rendered markdown confirmation the LLM emits to the user.
    #[serde(default)]
    pub narrative: String,
    /// Substrate's recommended next tool call (typically restore_identity).
    #[serde(default)]
    pub suggested_next_call: Option<SuggestedNextCall>,
    /// Per-response substrate metadata.
    #[serde(default)]
    pub meta: Option<ToolMeta>,
    /// Substrate lifecycle hints (`X-Erebyx-Hint` header).
    #[serde(default)]
    pub hints: Vec<String>,
    /// Tools the substrate ran automatically on this call
    /// (`X-Erebyx-Auto-Fired` header).
    #[serde(default)]
    pub auto_fired: Vec<String>,
    /// Raw JSON for any additional fields the API returns.
    #[serde(flatten)]
    pub extra: HashMap<String, serde_json::Value>,
}

/// A memory item to create during wrap-up.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WrapUpMemory {
    pub category: String,
    pub content: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub title: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub anchors: Option<Vec<String>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub importance: Option<f64>,
}

/// Builder for session wrap-up operations.
pub struct WrapUpBuilder<'a> {
    pub(crate) client: &'a crate::client::Memory,
    pub(crate) what_we_built: String,
    pub(crate) whats_next: String,
    pub(crate) diary: Option<String>,
    pub(crate) anchors: Option<Vec<String>>,
    pub(crate) energy: Option<String>,
    pub(crate) memories: Option<Vec<WrapUpMemory>>,
}

impl<'a> WrapUpBuilder<'a> {
    pub fn diary(mut self, diary: &str) -> Self {
        self.diary = Some(diary.to_string());
        self
    }

    pub fn anchors(mut self, anchors: Vec<&str>) -> Self {
        self.anchors = Some(anchors.into_iter().map(String::from).collect());
        self
    }

    pub fn energy(mut self, energy: &str) -> Self {
        self.energy = Some(energy.to_string());
        self
    }

    pub fn memories(mut self, memories: Vec<WrapUpMemory>) -> Self {
        self.memories = Some(memories);
        self
    }

    pub async fn send(self) -> Result<WrapUpResult, crate::Error> {
        self.client.execute_wrap_up(self).await
    }
}

// =========================================================================
// Restore identity types
// =========================================================================

/// Top-level identity slot returned to the AI on restore.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IdentityCore {
    /// The AI instance's chosen name.
    #[serde(default)]
    pub name: String,
    /// Tenant identifier the AI's memory is bound to.
    #[serde(default)]
    pub instance_id: String,
    /// The AI's origin story or self-defined opening anchor.
    #[serde(default)]
    pub origin_statement: String,
    /// One-paragraph rendered identity summary (model-readable).
    #[serde(default)]
    pub persona_summary: Option<String>,
}

/// A foundation memory carried into the AI's working context on restore.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FoundationMemoryItem {
    pub id: String,
    #[serde(default)]
    pub title: Option<String>,
    #[serde(default)]
    pub snippet: String,
    #[serde(default)]
    pub anchors: Vec<String>,
    #[serde(default)]
    pub captured_at: Option<String>,
}

/// Result of a restore_identity operation. Matches the substrate's
/// `RestoreIdentityResponse` shape one-to-one.
///
/// **Schema change (2026-05-27, Bucket B):** prior versions used the
/// legacy `GET /v0/identity` shape (flat `name + anchor + core_values + personality + family + foundation_anchors`).
/// The canonical substrate surface is `POST /v0/identity/restore` which returns the
/// full restore receipt — nested `identity (IdentityCore) + ethos + foundation_memories + topology + continuity + needs_onboarding + memory_guide + narrative + suggested_next_call`.
/// The old struct silently dropped `topology`, `continuity`,
/// `needs_onboarding`, `narrative`, `suggested_next_call`, and `meta`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RestoreIdentityResult {
    /// Nested identity core — name + instance_id + origin + persona.
    #[serde(default = "default_identity_core")]
    pub identity: IdentityCore,
    /// Principles, redlines, and values that anchor the AI.
    #[serde(default)]
    pub ethos: Vec<String>,
    /// Core memories carried into working context on restore.
    #[serde(default)]
    pub foundation_memories: Vec<FoundationMemoryItem>,
    /// Weighted concept priming for attention (categorical buckets only
    /// per the WHAT-not-HOW doctrine).
    #[serde(default)]
    pub topology: HashMap<String, serde_json::Value>,
    /// Continuity payload from the drift detector. Shape:
    /// `{last_session_ago, signals (≤10), signals_count}`.
    #[serde(default)]
    pub continuity: HashMap<String, serde_json::Value>,
    /// True only on the first call from a brand-new AI instance.
    #[serde(default)]
    pub needs_onboarding: bool,
    /// Optional full memory-system guide (opt-in via include_guide=true).
    #[serde(default)]
    pub memory_guide: Option<String>,
    /// Pre-rendered markdown narrative the LLM can drop straight into
    /// working context.
    #[serde(default)]
    pub narrative: String,
    /// Substrate's recommended next tool call.
    #[serde(default)]
    pub suggested_next_call: Option<SuggestedNextCall>,
    /// Per-response substrate metadata.
    #[serde(default)]
    pub meta: Option<ToolMeta>,
    /// Substrate lifecycle hints (`X-Erebyx-Hint` header).
    #[serde(default)]
    pub hints: Vec<String>,
    /// Tools the substrate ran automatically on this call
    /// (`X-Erebyx-Auto-Fired` header).
    #[serde(default)]
    pub auto_fired: Vec<String>,
    /// Raw JSON for any additional fields the API returns.
    #[serde(flatten)]
    pub extra: HashMap<String, serde_json::Value>,
}

fn default_identity_core() -> IdentityCore {
    IdentityCore {
        name: String::new(),
        instance_id: String::new(),
        origin_statement: String::new(),
        persona_summary: None,
    }
}

/// Builder for restore_identity operations.
pub struct RestoreIdentityBuilder<'a> {
    pub(crate) client: &'a crate::client::Memory,
    pub(crate) detail_level: Option<String>,
    pub(crate) include_guide: Option<bool>,
    pub(crate) limit: Option<u32>,
}

impl<'a> RestoreIdentityBuilder<'a> {
    pub fn detail_level(mut self, level: &str) -> Self {
        self.detail_level = Some(level.to_string());
        self
    }

    pub fn include_guide(mut self, include: bool) -> Self {
        self.include_guide = Some(include);
        self
    }

    pub fn limit(mut self, limit: u32) -> Self {
        self.limit = Some(limit);
        self
    }

    pub async fn send(self) -> Result<RestoreIdentityResult, crate::Error> {
        self.client.execute_restore_identity(self).await
    }
}

// =========================================================================
// Load context types
// =========================================================================

/// Most-recent handoff matching the load_context anchors.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HandoffSummary {
    pub id: String,
    #[serde(default)]
    pub summary: String,
    #[serde(default)]
    pub anchors: Vec<String>,
    #[serde(default)]
    pub captured_at: Option<String>,
}

/// A memory loaded into context as part of resuming work.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RelatedMemoryItem {
    pub id: String,
    #[serde(default)]
    pub title: Option<String>,
    #[serde(default)]
    pub snippet: String,
    #[serde(default)]
    pub anchors: Vec<String>,
    /// Categorical bucket: high / medium / low (WHAT-not-HOW).
    #[serde(default)]
    pub relevance_tier: String,
}

/// Result of a load_context operation. Matches the substrate's
/// `LoadContextResponse` shape one-to-one.
///
/// **Schema change (2026-05-27, Bucket B):** prior versions used the
/// legacy `GET /v0/handoff/context` shape (flat `session_id + status + anchors + energy + momentum_summary`).
/// The canonical substrate surface is `POST /v0/session/load` which returns the full session
/// resume payload — `session_id + anchors + handoff (HandoffSummary) + related_memories + skills + topology + narrative + suggested_next_call + meta`.
/// The old shape silently dropped
/// `handoff`, `related_memories`, `skills`, `topology`, `narrative`,
/// `suggested_next_call`, and `meta`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LoadContextResult {
    /// Session id generated or echoed on this call.
    #[serde(default)]
    pub session_id: String,
    /// Anchors that filtered this load (empty = most-recent absolute).
    #[serde(default)]
    pub anchors: Vec<String>,
    /// Most-recent matching handoff. None if no prior handoffs exist.
    #[serde(default)]
    pub handoff: Option<HandoffSummary>,
    /// Memories loaded into working context for this session.
    #[serde(default)]
    pub related_memories: Vec<RelatedMemoryItem>,
    /// Integrated skills relevant to the anchors.
    #[serde(default)]
    pub skills: Vec<String>,
    /// Concept priming for this session (categorical buckets).
    #[serde(default)]
    pub topology: HashMap<String, serde_json::Value>,
    /// Pre-rendered markdown the LLM can drop straight into context.
    #[serde(default)]
    pub narrative: String,
    /// Substrate's recommended next tool call.
    #[serde(default)]
    pub suggested_next_call: Option<SuggestedNextCall>,
    /// Per-response substrate metadata.
    #[serde(default)]
    pub meta: Option<ToolMeta>,
    /// Substrate lifecycle hints (`X-Erebyx-Hint` header).
    #[serde(default)]
    pub hints: Vec<String>,
    /// Tools the substrate ran automatically on this call
    /// (`X-Erebyx-Auto-Fired` header).
    #[serde(default)]
    pub auto_fired: Vec<String>,
    /// Raw JSON for any additional fields the API returns.
    #[serde(flatten)]
    pub extra: HashMap<String, serde_json::Value>,
}

/// Builder for load_context operations.
pub struct LoadContextBuilder<'a> {
    pub(crate) client: &'a crate::client::Memory,
    pub(crate) anchors: Option<Vec<String>>,
    pub(crate) mode: Option<String>,
    pub(crate) specialization_name: Option<String>,
    pub(crate) detail_level: Option<String>,
    pub(crate) load_priority: Option<String>,
}

impl<'a> LoadContextBuilder<'a> {
    pub fn anchors(mut self, anchors: Vec<&str>) -> Self {
        self.anchors = Some(anchors.into_iter().map(String::from).collect());
        self
    }

    // SIMPLIFY-MODES (Genesis Arche T-5 days, 2026-05-27): the
    // ``.mode()`` + ``.specialization_name()`` builders were dropped
    // to match the substrate's MCP launch surface narrowing (PR #963).
    // The ``mode`` MCP enum was a single value (``["session"]``) since
    // specialization-loading is a v0.2 feature; exposing the SDK
    // builder for it created drift vs MCP + advertised a knob with
    // one live value. Underlying fields stay ``pub(crate)`` for
    // substrate-internal HTTP routing. Re-exposed in v0.2 when
    // specialization-loading becomes a customer surface.

    pub fn detail_level(mut self, level: &str) -> Self {
        self.detail_level = Some(level.to_string());
        self
    }

    /// Token-budget priority hint for the substrate's load planner:
    /// `"minimal"` | `"summary"` | `"full"`.
    pub fn load_priority(mut self, priority: &str) -> Self {
        self.load_priority = Some(priority.to_string());
        self
    }

    pub async fn send(self) -> Result<LoadContextResult, crate::Error> {
        self.client.execute_load_context(self).await
    }
}