Skip to main content

kmp_application/memory/
types.rs

1use kmp_domain::{
2    DimensionSelection, ResolutionTier, TemporalAxis, TemporalCoordinate, TemporalCursor,
3    TemporalDirection, TemporalInterval, TemporalSelection, TemporalWindow,
4};
5
6use crate::queries::{GetNodeDetailResult, GraphRelationshipView};
7
8pub const DEFAULT_TRACE_PAGE_ENTRIES: usize = 64;
9/// A relate page counts facts, declared relations, coordinate relations and
10/// tensions alike; facts carry their text, so the page is kept smaller than
11/// a trace page.
12pub const DEFAULT_RELATE_PAGE_ENTRIES: usize = 32;
13pub const MAX_RELATE_PAGE_ENTRIES: usize = 256;
14pub const MAX_TRACE_PAGE_ENTRIES: usize = 256;
15
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct MemoryIngestCommand {
18    pub about: String,
19    pub memory: MemoryData,
20    pub provenance: Option<MemoryProvenanceData>,
21    pub idempotency_key: String,
22    pub dry_run: bool,
23    pub label_policy: LabelPolicy,
24    /// Writer diagnostics attached to the accepted command, never semantic memory.
25    pub receipt_context: Option<serde_json::Value>,
26}
27
28/// What an ingest does with a dimension that resembles a label the about
29/// already holds: the same identifier up to case and separators, or the
30/// same value under another key.
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
32pub enum LabelPolicy {
33    /// Write it and say so: `warnings` and `resembling_labels` name the
34    /// match, so vocabulary drift is seen when it happens.
35    #[default]
36    Warn,
37    /// Refuse the ingest naming the match, unless the dimension carries the
38    /// metadata that says the writer read the catalogue and insists.
39    Refuse,
40}
41
42/// A label a write named beside the existing label it resembles.
43#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
44pub struct ResemblingLabelData {
45    pub key: String,
46    pub value: String,
47    pub existing_key: String,
48    pub existing_value: String,
49    pub kind: String,
50    pub why: String,
51}
52
53#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
54pub struct MemoryData {
55    pub dimensions: Vec<MemoryDimensionData>,
56    pub entries: Vec<MemoryEntryData>,
57    pub relations: Vec<MemoryRelationData>,
58    pub evidence: Vec<MemoryEvidenceData>,
59}
60
61#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
62pub struct MemoryDimensionData {
63    pub id: String,
64    pub kind: String,
65    #[serde(skip_serializing_if = "Option::is_none")]
66    pub title: Option<String>,
67    #[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")]
68    pub metadata: std::collections::BTreeMap<String, String>,
69}
70
71#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
72pub struct MemoryEntryData {
73    pub id: String,
74    pub kind: String,
75    pub text: String,
76    pub coordinates: Vec<MemoryCoordinateData>,
77    #[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")]
78    pub metadata: std::collections::BTreeMap<String, String>,
79}
80
81#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
82pub struct MemoryCoordinateData {
83    pub dimension: String,
84    pub scope_id: String,
85    #[serde(skip_serializing_if = "Option::is_none")]
86    pub occurred_at: Option<String>,
87    #[serde(skip_serializing_if = "Option::is_none")]
88    pub observed_at: Option<String>,
89    #[serde(skip_serializing_if = "Option::is_none")]
90    pub ingested_at: Option<String>,
91    #[serde(skip_serializing_if = "Option::is_none")]
92    pub valid_from: Option<String>,
93    #[serde(skip_serializing_if = "Option::is_none")]
94    pub valid_until: Option<String>,
95    #[serde(skip_serializing_if = "Option::is_none")]
96    pub sequence: Option<u32>,
97    #[serde(skip_serializing_if = "Option::is_none")]
98    pub rank: Option<u32>,
99    #[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")]
100    pub metadata: std::collections::BTreeMap<String, String>,
101}
102
103#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
104pub struct MemoryRelationData {
105    #[serde(rename = "from")]
106    pub source_ref: String,
107    #[serde(rename = "to")]
108    pub target_ref: String,
109    pub rel: String,
110    #[serde(rename = "class")]
111    pub semantic_class: String,
112    #[serde(skip_serializing_if = "Option::is_none")]
113    pub why: Option<String>,
114    #[serde(skip_serializing_if = "Option::is_none")]
115    pub evidence: Option<String>,
116    #[serde(skip_serializing_if = "Option::is_none")]
117    pub confidence: Option<String>,
118    #[serde(skip_serializing_if = "Option::is_none")]
119    pub sequence: Option<u32>,
120    #[serde(skip_serializing_if = "Option::is_none")]
121    pub motivation: Option<String>,
122    #[serde(skip_serializing_if = "Option::is_none")]
123    pub method: Option<String>,
124    #[serde(skip_serializing_if = "Option::is_none")]
125    pub decision_id: Option<String>,
126    #[serde(skip_serializing_if = "Option::is_none")]
127    pub caused_by_node_id: Option<String>,
128    #[serde(skip_serializing_if = "Option::is_none")]
129    pub coordinate: Option<MemoryCoordinateData>,
130}
131
132#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
133pub struct MemoryEvidenceData {
134    pub id: String,
135    pub supports: Vec<String>,
136    pub text: String,
137    #[serde(skip_serializing_if = "Option::is_none")]
138    pub source: Option<String>,
139    #[serde(skip_serializing_if = "Option::is_none")]
140    pub time: Option<String>,
141    #[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")]
142    pub metadata: std::collections::BTreeMap<String, String>,
143}
144
145#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
146pub struct MemoryProvenanceData {
147    pub source_kind: String,
148    pub source_agent: String,
149    pub observed_at: String,
150    pub correlation_id: Option<String>,
151    pub causation_id: Option<String>,
152}
153
154#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
155pub struct MemoryAcceptedCounts {
156    pub entries: usize,
157    pub relations: usize,
158    pub evidence: usize,
159}
160
161#[derive(Debug, Clone, PartialEq, Eq)]
162pub struct MemoryIngestOutcome {
163    pub receipt_ref: Option<String>,
164    pub about: String,
165    pub memory_id: String,
166    pub accepted: MemoryAcceptedCounts,
167    pub read_after_write_ready: bool,
168    pub warnings: Vec<String>,
169    /// The dimension nodes this ingest declares for the first time, as
170    /// namespaced ids: the labels the write created rather than reused.
171    pub created_dimensions: Vec<String>,
172    /// Labels this ingest declared that resemble one the about already
173    /// holds, written under `LabelPolicy::Warn`.
174    pub resembling_labels: Vec<ResemblingLabelData>,
175}
176
177#[derive(Debug, Clone, PartialEq, Eq)]
178pub struct WakeMemoryQuery {
179    pub about: String,
180    pub role: String,
181    pub intent: String,
182    pub dimensions: DimensionSelection,
183    pub token_budget: u32,
184    pub depth: u32,
185    pub max_tier: Option<ResolutionTier>,
186    /// Cap on surfaced proof.evidence entries (None = unbounded). When set and
187    /// the about has more, Wake returns the first `max_entries` and reports the
188    /// withheld count via proof.frontier_size so the client near-expands.
189    pub max_entries: Option<usize>,
190    /// Which instants the packet stands on: the memory's frontier, one
191    /// instant, or a half-open span on one clock.
192    pub temporal: TemporalSelection,
193}
194
195#[derive(Debug, Clone, PartialEq, Eq)]
196pub struct AskMemoryQuery {
197    pub about: String,
198    pub question: String,
199    /// The user's own words when `question` is the agent's rendering of them
200    /// in the kernel's search language. Searched never; echoed and read
201    /// against the question so a rendering that lost something says so.
202    pub asked_as: Option<String>,
203    pub answer_policy: MemoryAnswerPolicy,
204    pub dimensions: DimensionSelection,
205    pub token_budget: u32,
206    pub depth: u32,
207    pub max_tier: Option<ResolutionTier>,
208    /// Cap on answer evidence entries after relevance filtering.
209    pub max_entries: Option<usize>,
210    /// Which instants the answer stands on: the memory's frontier, one
211    /// instant, or a half-open span on one clock. Only what the selection
212    /// admits competes, and the lifecycles are read as they stood then.
213    pub temporal: TemporalSelection,
214}
215
216#[derive(Debug, Clone, PartialEq, Eq)]
217pub struct TemporalMemoryQuery {
218    pub about: String,
219    pub direction: TemporalDirection,
220    pub axis: TemporalAxis,
221    pub cursor: Option<TemporalCursor>,
222    pub interval: Option<TemporalInterval>,
223    pub dimensions: DimensionSelection,
224    pub window: TemporalWindow,
225    pub limit_entries: Option<usize>,
226    pub include: TemporalIncludeOptions,
227    pub token_budget: u32,
228    pub depth: u32,
229    pub max_tier: Option<ResolutionTier>,
230}
231
232/// What memories of several abouts have to do with each other: the abouts
233/// the selection names or resolves, read within one span on one clock.
234#[derive(Debug, Clone, PartialEq, Eq)]
235pub struct RelateMemoryQuery {
236    pub about: String,
237    pub dimensions: DimensionSelection,
238    /// The span and clock the facts fall within; the memory's frontier when
239    /// the caller named none.
240    pub temporal: TemporalSelection,
241    pub token_budget: u32,
242    pub depth: u32,
243    pub max_tier: Option<ResolutionTier>,
244    pub page: RelatePageRequest,
245}
246
247#[derive(Debug, Clone, PartialEq, Eq, Default)]
248pub struct RelatePageRequest {
249    pub entries: Option<usize>,
250    pub cursor: Option<usize>,
251}
252
253impl RelatePageRequest {
254    pub fn offset(&self) -> usize {
255        self.cursor.unwrap_or_default()
256    }
257
258    pub fn entries_or_default(&self) -> usize {
259        self.entries.unwrap_or(DEFAULT_RELATE_PAGE_ENTRIES)
260    }
261}
262
263#[derive(Debug, Clone, PartialEq, Eq)]
264pub struct TraceMemoryQuery {
265    pub about: String,
266    pub from: String,
267    pub to: String,
268    pub role: String,
269    pub token_budget: u32,
270    pub page: TracePageRequest,
271}
272
273#[derive(Debug, Clone, PartialEq, Eq, Default)]
274pub struct TracePageRequest {
275    pub entries: Option<usize>,
276    pub cursor: Option<usize>,
277}
278
279impl TracePageRequest {
280    pub fn offset(&self) -> usize {
281        self.cursor.unwrap_or_default()
282    }
283
284    pub fn entries_or_default(&self) -> usize {
285        self.entries.unwrap_or(DEFAULT_TRACE_PAGE_ENTRIES)
286    }
287}
288
289#[derive(Debug, Clone, PartialEq, Eq)]
290pub struct InspectMemoryQuery {
291    pub about: String,
292    pub ref_id: String,
293    pub include_details: bool,
294    pub include_incoming: bool,
295    pub include_outgoing: bool,
296    pub include_raw: bool,
297}
298
299#[derive(Clone, PartialEq, Eq)]
300pub struct InspectMemoryResult {
301    pub detail: GetNodeDetailResult,
302    pub incoming: Vec<GraphRelationshipView>,
303    pub outgoing: Vec<GraphRelationshipView>,
304    pub evidence: Vec<InspectedEvidence>,
305    pub raw_coordinates: Vec<TemporalCoordinate>,
306    pub include_details: bool,
307    pub include_raw: bool,
308}
309
310#[derive(Clone, PartialEq, Eq)]
311pub struct InspectedEvidence {
312    pub detail: GetNodeDetailResult,
313    pub supports: Vec<String>,
314}
315
316#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
317pub enum MemoryAnswerPolicy {
318    #[default]
319    EvidenceOrUnknown,
320    ShowConflicts,
321    BestEffort,
322}
323
324#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
325pub struct TemporalIncludeOptions {
326    pub evidence: bool,
327    pub relations: bool,
328    pub raw_refs: bool,
329}
330
331#[derive(Debug, Clone, PartialEq)]
332pub struct TemporalMemoryResult {
333    pub traversal: kmp_domain::TemporalTraversalResult,
334    pub source_bundle: kmp_domain::KmpBundle,
335    pub include: TemporalIncludeOptions,
336    pub quality: kmp_domain::BundleQualityMetrics,
337}
338
339/// One label an entry stands in, as the pair a reader names it by: `key`
340/// is the dimension kind, `value` the bare scope id.
341#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Serialize)]
342pub struct EntryLabelData {
343    pub key: String,
344    pub value: String,
345}
346
347/// Change the labels an entry stands in without rewriting its text: labels
348/// to add, labels to take off, and why. Translated against what the about
349/// holds, like an ingest, into one change the log keeps.
350#[derive(Debug, Clone, PartialEq, Eq)]
351pub struct MemoryRelabelCommand {
352    pub about: String,
353    pub ref_id: String,
354    pub add: Vec<EntryLabelData>,
355    pub remove: Vec<EntryLabelData>,
356    pub why: String,
357    pub provenance: Option<MemoryProvenanceData>,
358    pub idempotency_key: String,
359    pub dry_run: bool,
360    pub label_policy: LabelPolicy,
361    /// Label keys the writer insists are new even where the catalogue holds
362    /// one that resembles them: it read the catalogue and means something
363    /// else. Those labels are left out of the resemblance check.
364    pub intended_new: std::collections::BTreeSet<String>,
365}
366
367#[derive(Debug, Clone, PartialEq, Eq)]
368pub struct MemoryRelabelOutcome {
369    pub about: String,
370    pub ref_id: String,
371    pub added: Vec<EntryLabelData>,
372    pub removed: Vec<EntryLabelData>,
373    /// Every label the entry stands in after this relabel, by key then value.
374    pub labels: Vec<EntryLabelData>,
375    /// The dimension nodes this relabel declared for the first time, as
376    /// namespaced ids: the labels it created rather than reused.
377    pub created_dimensions: Vec<String>,
378    /// Labels this relabel added that resemble one the about already holds,
379    /// written under `LabelPolicy::Warn`.
380    pub resembling_labels: Vec<ResemblingLabelData>,
381    pub read_after_write_ready: bool,
382    pub warnings: Vec<String>,
383}